From 848fe763c4d72f052149bcc2a732129ac608b281 Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Mon, 26 Jun 2023 12:24:14 -0300 Subject: [PATCH] Connectors: Implement Uniswap v3 swap connector (#18) --- .../swap/uniswap-v3/IUniswapV3Factory.sol | 15 ++ .../IUniswapV3PeripheryImmutableState.sol | 10 + .../swap/uniswap-v3/IUniswapV3SwapRouter.sol | 36 ++++ .../swap/uniswap-v3/UniswapV3Connector.sol | 178 ++++++++++++++++++ .../uniswap-v3/UniswapV3Connector.behavior.ts | 116 ++++++++++++ .../uniswap-v3/UniswapV3Connector.mainnet.ts | 37 ++++ .../uniswap-v3/UniswapV3Connector.polygon.ts | 37 ++++ .../contracts/test/utils/ArraysMock.sol | 4 + .../contracts/test/utils/BytesHelpersMock.sol | 31 +++ packages/helpers/contracts/utils/Arrays.sol | 12 ++ .../helpers/contracts/utils/BytesHelpers.sol | 23 ++- .../test/contracts/utils/Arrays.test.ts | 36 +++- .../test/contracts/utils/BytesHelpers.test.ts | 46 +++++ 13 files changed, 573 insertions(+), 8 deletions(-) create mode 100644 packages/connectors/contracts/swap/uniswap-v3/IUniswapV3Factory.sol create mode 100644 packages/connectors/contracts/swap/uniswap-v3/IUniswapV3PeripheryImmutableState.sol create mode 100644 packages/connectors/contracts/swap/uniswap-v3/IUniswapV3SwapRouter.sol create mode 100644 packages/connectors/contracts/swap/uniswap-v3/UniswapV3Connector.sol create mode 100644 packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.behavior.ts create mode 100644 packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.mainnet.ts create mode 100644 packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.polygon.ts create mode 100644 packages/helpers/contracts/test/utils/BytesHelpersMock.sol create mode 100644 packages/helpers/test/contracts/utils/BytesHelpers.test.ts diff --git a/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3Factory.sol b/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3Factory.sol new file mode 100644 index 00000000..817b0275 --- /dev/null +++ b/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3Factory.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.5.0; + +/// @title The interface for the Uniswap V3 Factory +/// @notice The Uniswap V3 Factory facilitates creation of Uniswap V3 pools and control over the protocol fees +interface IUniswapV3Factory { + /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist + /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order + /// @param tokenA The contract address of either token0 or token1 + /// @param tokenB The contract address of the other token + /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip + /// @return pool The pool address + function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool); +} diff --git a/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3PeripheryImmutableState.sol b/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3PeripheryImmutableState.sol new file mode 100644 index 00000000..0eb7a3cb --- /dev/null +++ b/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3PeripheryImmutableState.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.5.0; + +/// @title Immutable state +/// @notice Functions that return immutable state of the router +interface IUniswapV3PeripheryImmutableState { + /// @return Returns the address of the Uniswap V3 factory + function factory() external view returns (address); +} diff --git a/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3SwapRouter.sol b/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3SwapRouter.sol new file mode 100644 index 00000000..e7d11c11 --- /dev/null +++ b/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3SwapRouter.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.7.5; + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface IUniswapV3SwapRouter { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); +} diff --git a/packages/connectors/contracts/swap/uniswap-v3/UniswapV3Connector.sol b/packages/connectors/contracts/swap/uniswap-v3/UniswapV3Connector.sol new file mode 100644 index 00000000..726ca973 --- /dev/null +++ b/packages/connectors/contracts/swap/uniswap-v3/UniswapV3Connector.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import '@mimic-fi/v3-helpers/contracts/math/UncheckedMath.sol'; +import '@mimic-fi/v3-helpers/contracts/utils/Arrays.sol'; +import '@mimic-fi/v3-helpers/contracts/utils/BytesHelpers.sol'; +import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; + +import './IUniswapV3Factory.sol'; +import './IUniswapV3SwapRouter.sol'; +import './IUniswapV3PeripheryImmutableState.sol'; + +/** + * @title UniswapV3Connector + * @dev Interfaces with Uniswap V3 to swap tokens + */ +contract UniswapV3Connector { + using BytesHelpers for bytes; + using UncheckedMath for uint256; + + // Reference to UniswapV3 router + IUniswapV3SwapRouter public immutable uniswapV3Router; + + /** + * @dev Initializes the UniswapV3Connector contract + * @param _uniswapV3Router Uniswap V3 router reference + */ + constructor(address _uniswapV3Router) { + uniswapV3Router = IUniswapV3SwapRouter(_uniswapV3Router); + } + + /** + * @dev Executes a token swap in Uniswap V3 + * @param tokenIn Token being sent + * @param tokenOut Token being received + * @param amountIn Amount of tokenIn being swapped + * @param minAmountOut Minimum amount of tokenOut willing to receive + * @param fee Fee to be used + * @param hopTokens Optional list of hop-tokens between tokenIn and tokenOut, only used for multi-hops + * @param hopFees Optional list of hop-fees between tokenIn and tokenOut, only used for multi-hops + */ + function execute( + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + uint24 fee, + address[] memory hopTokens, + uint24[] memory hopFees + ) external returns (uint256 amountOut) { + require(tokenIn != tokenOut, 'UNI_V3_SWAP_SAME_TOKEN'); + require(hopTokens.length == hopFees.length, 'UNI_V3_BAD_HOP_TOKENS_FEES_LEN'); + + uint256 preBalanceIn = IERC20(tokenIn).balanceOf(address(this)); + uint256 preBalanceOut = IERC20(tokenOut).balanceOf(address(this)); + + ERC20Helpers.approve(tokenIn, address(uniswapV3Router), amountIn); + hopTokens.length == 0 + ? _singleSwap(tokenIn, tokenOut, amountIn, minAmountOut, fee) + : _batchSwap(tokenIn, tokenOut, amountIn, minAmountOut, fee, hopTokens, hopFees); + + uint256 postBalanceIn = IERC20(tokenIn).balanceOf(address(this)); + require(postBalanceIn >= preBalanceIn - amountIn, 'UNI_V3_BAD_TOKEN_IN_BALANCE'); + + uint256 postBalanceOut = IERC20(tokenOut).balanceOf(address(this)); + amountOut = postBalanceOut - preBalanceOut; + require(amountOut >= minAmountOut, 'UNI_V3_MIN_AMOUNT_OUT'); + } + + /** + * @dev Swap two tokens through UniswapV3 using a single hop + * @param tokenIn Token being sent + * @param tokenOut Token being received + * @param amountIn Amount of tokenIn being swapped + * @param minAmountOut Minimum amount of tokenOut willing to receive + * @param fee Fee to be used + */ + function _singleSwap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, uint24 fee) + internal + returns (uint256 amountOut) + { + _validatePool(_uniswapV3Factory(), tokenIn, tokenOut, fee); + + IUniswapV3SwapRouter.ExactInputSingleParams memory input; + input.tokenIn = tokenIn; + input.tokenOut = tokenOut; + input.fee = fee; + input.recipient = address(this); + input.deadline = block.timestamp; + input.amountIn = amountIn; + input.amountOutMinimum = minAmountOut; + input.sqrtPriceLimitX96 = 0; + return uniswapV3Router.exactInputSingle(input); + } + + /** + * @dev Swap two tokens through UniswapV3 using a multi hop + * @param tokenIn Token being sent + * @param tokenOut Token being received + * @param amountIn Amount of the first token in the path to be swapped + * @param minAmountOut Minimum amount of the last token in the path willing to receive + * @param fee Fee to be used + * @param hopTokens List of hop-tokens between tokenIn and tokenOut + * @param hopFees List of hop-fees between tokenIn and tokenOut + */ + function _batchSwap( + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + uint24 fee, + address[] memory hopTokens, + uint24[] memory hopFees + ) internal returns (uint256 amountOut) { + address factory = _uniswapV3Factory(); + address[] memory tokens = Arrays.from(tokenIn, hopTokens, tokenOut); + uint24[] memory fees = Arrays.from(fee, hopFees); + + // No need for checked math since we are using it to compute indexes manually, always within boundaries + for (uint256 i = 0; i < fees.length; i = i.uncheckedAdd(1)) { + _validatePool(factory, tokens[i], tokens[i.uncheckedAdd(1)], fees[i]); + } + + IUniswapV3SwapRouter.ExactInputParams memory input; + input.path = _encodePoolPath(tokens, fees); + input.amountIn = amountIn; + input.amountOutMinimum = minAmountOut; + input.recipient = address(this); + input.deadline = block.timestamp; + return uniswapV3Router.exactInput(input); + } + + /** + * @dev Tells the Uniswap V3 factory contract address + * @return Address of the Uniswap V3 factory contract + */ + function _uniswapV3Factory() internal view returns (address) { + return IUniswapV3PeripheryImmutableState(address(uniswapV3Router)).factory(); + } + + /** + * @dev Validates that there is a pool created for tokenA and tokenB with a requested fee + * @param factory UniswapV3 factory to check against + * @param tokenA One of the tokens in the pool + * @param tokenB The other token in the pool + * @param fee Fee used by the pool + */ + function _validatePool(address factory, address tokenA, address tokenB, uint24 fee) internal view { + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(IUniswapV3Factory(factory).getPool(token0, token1, fee) != address(0), 'UNI_V3_INVALID_POOL_FEE'); + } + + /** + * @dev Encodes a path of tokens with their corresponding fees + * @param tokens List of tokens to be encoded + * @param fees List of fees to use for each token pair + */ + function _encodePoolPath(address[] memory tokens, uint24[] memory fees) internal pure returns (bytes memory path) { + path = new bytes(0); + for (uint256 i = 0; i < fees.length; i = i.uncheckedAdd(1)) path = path.concat(tokens[i]).concat(fees[i]); + path = path.concat(tokens[fees.length]); + } +} diff --git a/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.behavior.ts b/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.behavior.ts new file mode 100644 index 00000000..bcfef2f4 --- /dev/null +++ b/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.behavior.ts @@ -0,0 +1,116 @@ +import { deployProxy, fp, impersonate, instanceAt, pct, toUSDC, toWBTC, ZERO_ADDRESS } from '@mimic-fi/v3-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, Contract } from 'ethers' + +export function itBehavesLikeUniswapV3Connector( + USDC: string, + WETH: string, + WBTC: string, + WHALE: string, + SLIPPAGE: number, + WETH_USDC_FEE: number, + WETH_WBTC_FEE: number, + CHAINLINK_USDC_ETH: string, + CHAINLINK_WBTC_ETH: string +): void { + let weth: Contract, usdc: Contract, wbtc: Contract, whale: SignerWithAddress, priceOracle: Contract + + before('load tokens and accounts', async function () { + weth = await instanceAt('IERC20Metadata', WETH) + wbtc = await instanceAt('IERC20Metadata', WBTC) + usdc = await instanceAt('IERC20Metadata', USDC) + whale = await impersonate(WHALE, fp(100)) + }) + + before('create price oracle', async function () { + priceOracle = await deployProxy( + '@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle', + [], + [ + ZERO_ADDRESS, + ZERO_ADDRESS, + WETH, + [ + { base: USDC, quote: WETH, feed: CHAINLINK_USDC_ETH }, + { base: WBTC, quote: WETH, feed: CHAINLINK_WBTC_ETH }, + ], + ] + ) + }) + + const getExpectedMinAmountOut = async ( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + slippage: number + ): Promise => { + const price = await priceOracle['getPrice(address,address)'](tokenIn, tokenOut) + const expectedAmountOut = price.mul(amountIn).div(fp(1)) + return expectedAmountOut.sub(pct(expectedAmountOut, slippage)) + } + + context('single swap', () => { + context('USDC-WETH', () => { + const amountIn = toUSDC(10e3) + + it('swaps correctly', async function () { + const previousBalance = await weth.balanceOf(this.connector.address) + await usdc.connect(whale).transfer(this.connector.address, amountIn) + + await this.connector.connect(whale).execute(USDC, WETH, amountIn, 0, WETH_USDC_FEE, [], []) + + const currentBalance = await weth.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(USDC, WETH, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + + context('WETH-USDC', () => { + const amountIn = fp(1) + + it('swaps correctly', async function () { + const previousBalance = await usdc.balanceOf(this.connector.address) + await weth.connect(whale).transfer(this.connector.address, amountIn) + + await this.connector.connect(whale).execute(WETH, USDC, amountIn, 0, WETH_USDC_FEE, [], []) + + const currentBalance = await usdc.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(WETH, USDC, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + }) + + context('batch swap', () => { + context('USDC-WBTC', () => { + const amountIn = toUSDC(10e3) + + it('swaps correctly', async function () { + const previousBalance = await wbtc.balanceOf(this.connector.address) + await usdc.connect(whale).transfer(this.connector.address, amountIn) + + await this.connector.connect(whale).execute(USDC, WBTC, amountIn, 0, WETH_USDC_FEE, [WETH], [WETH_WBTC_FEE]) + + const currentBalance = await wbtc.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(USDC, WBTC, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + + context('WBTC-USDC', () => { + const amountIn = toWBTC(1) + + it('swaps correctly', async function () { + const previousBalance = await usdc.balanceOf(this.connector.address) + await wbtc.connect(whale).transfer(this.connector.address, amountIn) + + await this.connector.connect(whale).execute(WBTC, USDC, amountIn, 0, WETH_USDC_FEE, [WETH], [WETH_WBTC_FEE]) + + const currentBalance = await usdc.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(WBTC, USDC, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + }) +} diff --git a/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.mainnet.ts b/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.mainnet.ts new file mode 100644 index 00000000..2464977d --- /dev/null +++ b/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.mainnet.ts @@ -0,0 +1,37 @@ +import { deploy } from '@mimic-fi/v3-helpers' + +import { itBehavesLikeUniswapV3Connector } from './UniswapV3Connector.behavior' + +/* eslint-disable no-secrets/no-secrets */ + +const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' +const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' +const WBTC = '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' +const WHALE = '0xf584f8728b874a6a5c7a8d4d387c9aae9172d621' + +const UNISWAP_V3_ROUTER = '0xE592427A0AEce92De3Edee1F18E0157C05861564' + +const CHAINLINK_USDC_ETH = '0x986b5E1e1755e3C2440e960477f25201B0a8bbD4' +const CHAINLINK_WBTC_ETH = '0xdeb288F737066589598e9214E782fa5A8eD689e8' + +describe('UniswapV3Connector', () => { + const SLIPPAGE = 0.02 + const WETH_USDC_FEE = 3000 + const WETH_WBTC_FEE = 3000 + + before('create uniswap v3 connector', async function () { + this.connector = await deploy('UniswapV3Connector', [UNISWAP_V3_ROUTER]) + }) + + itBehavesLikeUniswapV3Connector( + USDC, + WETH, + WBTC, + WHALE, + SLIPPAGE, + WETH_USDC_FEE, + WETH_WBTC_FEE, + CHAINLINK_USDC_ETH, + CHAINLINK_WBTC_ETH + ) +}) diff --git a/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.polygon.ts b/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.polygon.ts new file mode 100644 index 00000000..a7328c22 --- /dev/null +++ b/packages/connectors/test/swap/uniswap-v3/UniswapV3Connector.polygon.ts @@ -0,0 +1,37 @@ +import { deploy } from '@mimic-fi/v3-helpers' + +import { itBehavesLikeUniswapV3Connector } from './UniswapV3Connector.behavior' + +/* eslint-disable no-secrets/no-secrets */ + +const USDC = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' +const WETH = '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619' +const WBTC = '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6' +const WHALE = '0x21cb017b40abe17b6dfb9ba64a3ab0f24a7e60ea' + +const UNISWAP_V3_ROUTER = '0xE592427A0AEce92De3Edee1F18E0157C05861564' + +const CHAINLINK_USDC_ETH = '0xefb7e6be8356ccc6827799b6a7348ee674a80eae' +const CHAINLINK_WBTC_ETH = '0x19b0F0833C78c0848109E3842D34d2fDF2cA69BA' + +describe('UniswapV3Connector', () => { + const SLIPPAGE = 0.02 + const WETH_USDC_FEE = 3000 + const WETH_WBTC_FEE = 3000 + + before('create uniswap v3 connector', async function () { + this.connector = await deploy('UniswapV3Connector', [UNISWAP_V3_ROUTER]) + }) + + itBehavesLikeUniswapV3Connector( + USDC, + WETH, + WBTC, + WHALE, + SLIPPAGE, + WETH_USDC_FEE, + WETH_WBTC_FEE, + CHAINLINK_USDC_ETH, + CHAINLINK_WBTC_ETH + ) +}) diff --git a/packages/helpers/contracts/test/utils/ArraysMock.sol b/packages/helpers/contracts/test/utils/ArraysMock.sol index cfbcfd73..62856c2a 100644 --- a/packages/helpers/contracts/test/utils/ArraysMock.sol +++ b/packages/helpers/contracts/test/utils/ArraysMock.sol @@ -24,4 +24,8 @@ library ArraysMock { function from2(address a, address[] memory b, address c) external pure returns (address[] memory result) { return Arrays.from(a, b, c); } + + function from3(uint24 a, uint24[] memory b) external pure returns (uint24[] memory result) { + return Arrays.from(a, b); + } } diff --git a/packages/helpers/contracts/test/utils/BytesHelpersMock.sol b/packages/helpers/contracts/test/utils/BytesHelpersMock.sol new file mode 100644 index 00000000..6607c5d1 --- /dev/null +++ b/packages/helpers/contracts/test/utils/BytesHelpersMock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +import '../../utils/BytesHelpers.sol'; + +library BytesHelpersMock { + function concat1(bytes memory self, address value) external pure returns (bytes memory) { + return BytesHelpers.concat(self, value); + } + + function concat2(bytes memory self, uint24 value) external pure returns (bytes memory) { + return BytesHelpers.concat(self, value); + } + + function toUint256(bytes memory self, uint256 start) external pure returns (uint256) { + return BytesHelpers.toUint256(self, start); + } +} diff --git a/packages/helpers/contracts/utils/Arrays.sol b/packages/helpers/contracts/utils/Arrays.sol index b79e99fd..8b45b2a1 100644 --- a/packages/helpers/contracts/utils/Arrays.sol +++ b/packages/helpers/contracts/utils/Arrays.sol @@ -44,4 +44,16 @@ library Arrays { for (uint256 i = 0; i < b.length; i = i.uncheckedAdd(1)) result[i.uncheckedAdd(1)] = b[i]; result[b.length.uncheckedAdd(1)] = c; } + + /** + * @dev Builds an array of uint24s based on the given ones + */ + function from(uint24 a, uint24[] memory b) internal pure returns (uint24[] memory result) { + // No need for checked math since we are simply adding one to a memory array's length + result = new uint24[](b.length.uncheckedAdd(1)); + result[0] = a; + + // No need for checked math since we are using it to compute indexes manually, always within boundaries + for (uint256 i = 0; i < b.length; i = i.uncheckedAdd(1)) result[i.uncheckedAdd(1)] = b[i]; + } } diff --git a/packages/helpers/contracts/utils/BytesHelpers.sol b/packages/helpers/contracts/utils/BytesHelpers.sol index ab9b257f..67f966ce 100644 --- a/packages/helpers/contracts/utils/BytesHelpers.sol +++ b/packages/helpers/contracts/utils/BytesHelpers.sol @@ -19,10 +19,27 @@ pragma solidity ^0.8.0; * @dev Provides a list of Bytes helper methods */ library BytesHelpers { - function toUint256(bytes memory self, uint256 offset) internal pure returns (uint256 result) { - require(self.length >= offset + 32, 'BYTES_OUT_OF_BOUNDS'); + /** + * @dev Concatenates an address to a bytes array + */ + function concat(bytes memory self, address value) internal pure returns (bytes memory) { + return abi.encodePacked(self, value); + } + + /** + * @dev Concatenates an uint24 to a bytes array + */ + function concat(bytes memory self, uint24 value) internal pure returns (bytes memory) { + return abi.encodePacked(self, value); + } + + /** + * @dev Reads an uint256 from a bytes array starting at a given position + */ + function toUint256(bytes memory self, uint256 start) internal pure returns (uint256 result) { + require(self.length >= start + 32, 'BYTES_OUT_OF_BOUNDS'); assembly { - result := mload(add(add(self, 0x20), offset)) + result := mload(add(add(self, 0x20), start)) } } } diff --git a/packages/helpers/test/contracts/utils/Arrays.test.ts b/packages/helpers/test/contracts/utils/Arrays.test.ts index 41d58b06..b541d133 100644 --- a/packages/helpers/test/contracts/utils/Arrays.test.ts +++ b/packages/helpers/test/contracts/utils/Arrays.test.ts @@ -10,12 +10,12 @@ describe('Arrays', () => { library = await deploy('ArraysMock') }) - const ADDR_1 = '0x0000000000000000000000000000000000000001' - const ADDR_2 = '0x0000000000000000000000000000000000000002' - const ADDR_3 = '0x0000000000000000000000000000000000000003' - const ADDR_4 = '0x0000000000000000000000000000000000000004' - describe('from', () => { + const ADDR_1 = '0x0000000000000000000000000000000000000001' + const ADDR_2 = '0x0000000000000000000000000000000000000002' + const ADDR_3 = '0x0000000000000000000000000000000000000003' + const ADDR_4 = '0x0000000000000000000000000000000000000004' + it('concatenates two addresses correctly', async () => { const result = await library.from1(ADDR_1, ADDR_2) @@ -34,4 +34,30 @@ describe('Arrays', () => { expect(result[3]).to.be.equal(ADDR_4) }) }) + + describe('from', () => { + it('concatenates correctly', async () => { + const result = await library.from3(1, []) + + expect(result.length).to.be.equal(1) + expect(result[0]).to.be.equal(1) + }) + + it('concatenates correctly', async () => { + const result = await library.from3(1, [2]) + + expect(result.length).to.be.equal(2) + expect(result[0]).to.be.equal(1) + expect(result[1]).to.be.equal(2) + }) + + it('concatenates correctly', async () => { + const result = await library.from3(1, [2, 3]) + + expect(result.length).to.be.equal(3) + expect(result[0]).to.be.equal(1) + expect(result[1]).to.be.equal(2) + expect(result[2]).to.be.equal(3) + }) + }) }) diff --git a/packages/helpers/test/contracts/utils/BytesHelpers.test.ts b/packages/helpers/test/contracts/utils/BytesHelpers.test.ts new file mode 100644 index 00000000..1551ffd8 --- /dev/null +++ b/packages/helpers/test/contracts/utils/BytesHelpers.test.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai' +import { Contract } from 'ethers' +import { hexlify, hexZeroPad } from 'ethers/lib/utils' + +import { deploy } from '../../../' + +describe('BytesHelpers', () => { + let library: Contract + + beforeEach('deploy lib', async () => { + library = await deploy('BytesHelpersMock') + }) + + describe('toUint256', () => { + const bytes = + '0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002' + + it('extracts an uint256 correctly', async () => { + expect(await library.toUint256(bytes, 0)).to.be.equal(1) + expect(await library.toUint256(bytes, 32)).to.be.equal(2) + }) + + it('reverts if out of bounds', async () => { + await expect(library.toUint256(bytes, 64)).to.be.revertedWith('BYTES_OUT_OF_BOUNDS') + await expect(library.toUint256(bytes, 33)).to.be.revertedWith('BYTES_OUT_OF_BOUNDS') + }) + }) + + describe('concat', () => { + const array = '0xabcdef' + + it('concatenates an address with a bytes array', async () => { + const address = '0xffffffffffffffffffffffffffffffffffffffff' + const result = await library.concat1(array, address) + + expect(result).to.be.equal(array + address.slice(2)) + }) + + it('concatenates an uint24 with a bytes array', async () => { + const number = 5 + const result = await library.concat2(array, number) + + expect(result).to.be.equal(array + hexZeroPad(hexlify(number), 3).slice(2)) + }) + }) +})