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))
+ })
+ })
+})