From 245a844203d1e8b8a34ec9efa0c4cfd3e722eff4 Mon Sep 17 00:00:00 2001 From: viatrix Date: Mon, 14 Oct 2024 12:40:54 +0300 Subject: [PATCH 01/28] WIP: Add swap adapter --- contracts/adapters/SwapAdapter.sol | 231 ++++++++++++++++++ .../interfaces/INativeTokenAdapter.sol | 20 ++ .../adapters/interfaces/IV3SwapRouter.sol | 67 +++++ contracts/adapters/interfaces/IWETH.sol | 5 + testUnderForked/swapAdapter.js | 19 ++ 5 files changed, 342 insertions(+) create mode 100644 contracts/adapters/SwapAdapter.sol create mode 100644 contracts/adapters/interfaces/INativeTokenAdapter.sol create mode 100644 contracts/adapters/interfaces/IV3SwapRouter.sol create mode 100644 contracts/adapters/interfaces/IWETH.sol create mode 100644 testUnderForked/swapAdapter.js diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol new file mode 100644 index 00000000..77ab10d7 --- /dev/null +++ b/contracts/adapters/SwapAdapter.sol @@ -0,0 +1,231 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../../contracts/interfaces/IBridge.sol"; +import "../../contracts/interfaces/IFeeHandler.sol"; +import "../../contracts/adapters/interfaces/INativeTokenAdapter.sol"; +import "../../contracts/adapters/interfaces/IWETH.sol"; +import "../../contracts/adapters/interfaces/IV3SwapRouter.sol"; + +contract SwapAdapter is AccessControl { + + using SafeERC20 for IERC20; + + IBridge public immutable _bridge; + bytes32 public immutable _resourceIDEth; + address immutable _weth; + IV3SwapRouter public _swapRouter; + + mapping(address => bytes32) public tokenToResourceID; + + error CallerNotAdmin(); + error AlreadySet(); + error TokenInvalid(); + error PathInvalid(); + error MsgValueLowerThanFee(uint256 value); + error InsufficientAmount(uint256 amount); + error FailedFundsTransfer(); + error AmountLowerThanFee(uint256 amount); + + event TokenResourceIDSet(address token, bytes32 resourceID); + + constructor( + IBridge bridge, + bytes32 resourceIDEth, + address weth, + IV3SwapRouter swapRouter + ) { + _bridge = bridge; + _resourceIDEth = resourceIDEth; + _weth = weth; + _swapRouter = swapRouter; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + modifier onlyAdmin() { + if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert CallerNotAdmin(); + _; + } + + // Admin functions + function setTokenResourceID(address token, bytes32 resourceID) external onlyAdmin { + if (tokenToResourceID[token] == resourceID) revert AlreadySet(); + tokenToResourceID[token] = resourceID; + emit TokenResourceIDSet(token, resourceID); + } + + function depositTokensToEth( + uint8 destinationDomainID, + address recipient, + uint256 gas, + address token, + uint256 tokenAmount, + uint256 amountOutMinimum, + address[] memory pathTokens, + uint24[] memory pathFees + ) external payable { + if (tokenToResourceID[token] == bytes32(0)) revert TokenInvalid(); + + // Swap all tokens to ETH (exact input) + IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); + IERC20(token).safeApprove(address(_swapRouter), tokenAmount); + + uint256 amount; + + { + bytes memory path = _verifyAndEncodePath( + pathTokens, + pathFees, + token, + _weth + ); + IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + amountIn: tokenAmount, + amountOutMinimum: amountOutMinimum + }); + + amount = _swapRouter.exactInput(params); + } + + IWETH(_weth).withdraw(amount); + + // Make Native Token deposit + if (amount == 0) revert InsufficientAmount(amount); + bytes memory depositDataAfterAmount = abi.encodePacked( + uint256(20), + recipient + ); + uint256 fee; + { + address feeHandlerRouter = _bridge._feeHandler(); + (fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + _resourceIDEth, + abi.encodePacked(amount, depositDataAfterAmount), + "" // feeData - not parsed + ); + } + + if (amount < fee) revert AmountLowerThanFee(amount); + amount -= fee; + + bytes memory depositData = abi.encodePacked( + amount, + depositDataAfterAmount + ); + + _bridge.deposit{value: fee}(destinationDomainID, _resourceIDEth, depositData, ""); + + address nativeHandlerAddress = _bridge._resourceIDToHandlerAddress(_resourceIDEth); + (bool success, ) = nativeHandlerAddress.call{value: amount}(""); + if (!success) revert FailedFundsTransfer(); + + // Return unspent fee to msg.sender + uint256 leftover = address(this).balance; + if (leftover > 0) { + (success, ) = payable(msg.sender).call{value: leftover}(""); + // Do not revert if sender does not want to receive. + } + } + + function depositEthToTokens( + uint8 destinationDomainID, + address recipient, + address token, + uint256 amountOutMinimum, + address[] memory pathTokens, + uint24[] memory pathFees + ) external payable { + bytes32 resourceID = tokenToResourceID[token]; + if (resourceID == bytes32(0)) revert TokenInvalid(); + + // Compose depositData + bytes memory depositDataAfterAmount = abi.encodePacked( + uint256(20), + recipient + ); + if (msg.value == 0) revert InsufficientAmount(msg.value); + uint256 fee; + { + address feeHandlerRouter = _bridge._feeHandler(); + (fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + resourceID, + abi.encodePacked(msg.value, depositDataAfterAmount), + "" // feeData - not parsed + ); + } + + + if (msg.value < fee) revert MsgValueLowerThanFee(msg.value); + uint256 amountOut; + { + uint256 swapAmount = msg.value - fee; + // Convert everything except the fee + + // Swap ETH to tokens (exact input) + bytes memory path = _verifyAndEncodePath( + pathTokens, + pathFees, + _weth, + token + ); + IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + amountIn: swapAmount, + amountOutMinimum: amountOutMinimum + }); + + amountOut = _swapRouter.exactInput{value: swapAmount}(params); + } + + bytes memory depositData = abi.encodePacked( + amountOut, + depositDataAfterAmount + ); + + address ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(resourceID); + IERC20(token).safeApprove(address(ERC20HandlerAddress), amountOut); + _bridge.deposit{value: fee}(destinationDomainID, resourceID, depositData, ""); + + // Return unspent fee to msg.sender + uint256 leftover = address(this).balance; + if (leftover > 0) { + payable(msg.sender).call{value: leftover}(""); + // Do not revert if sender does not want to receive. + } + } + + function _verifyAndEncodePath( + address[] memory tokens, + uint24[] memory fees, + address tokenIn, + address tokenOut + ) internal view returns (bytes memory path) { + if (tokens.length != fees.length + 1) { + revert PathInvalid(); + } + + tokenIn = tokenIn == address(0) ? address(_weth) : tokenIn; + if (tokens[tokens.length - 1] != tokenIn) revert PathInvalid(); + + tokenOut = tokenOut == address(0) ? address(_weth) : tokenOut; + if (tokens[0] != tokenOut) revert PathInvalid(); + + for (uint256 i = 0; i < tokens.length - 1; i++){ + path = abi.encodePacked(path, tokens[i], fees[i]); + } + path = abi.encodePacked(path, tokens[tokens.length - 1]); + } +} diff --git a/contracts/adapters/interfaces/INativeTokenAdapter.sol b/contracts/adapters/interfaces/INativeTokenAdapter.sol new file mode 100644 index 00000000..3a52a13f --- /dev/null +++ b/contracts/adapters/interfaces/INativeTokenAdapter.sol @@ -0,0 +1,20 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +/** + @title Interface for Bridge contract. + @author ChainSafe Systems. + */ +interface IGmpTransferAdapter { + /** + @notice Makes a native token deposit with an included message. + @param destinationDomainID ID of destination chain. + @param recipientAddress Address that will receive native tokens on destination chain. + */ + + function depositToEVM( + uint8 destinationDomainID, + address recipientAddress + ) external payable; +} diff --git a/contracts/adapters/interfaces/IV3SwapRouter.sol b/contracts/adapters/interfaces/IV3SwapRouter.sol new file mode 100644 index 00000000..063055f7 --- /dev/null +++ b/contracts/adapters/interfaces/IV3SwapRouter.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface IV3SwapRouter { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, + /// and swap the entire amount, enabling contracts to send tokens before calling this function. + /// @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 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, + /// and swap the entire amount, enabling contracts to send tokens before calling this function. + /// @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); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// that may remain in the router after the swap. + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) + /// that may remain in the router after the swap. + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata + /// @return amountIn The amount of the input token + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} \ No newline at end of file diff --git a/contracts/adapters/interfaces/IWETH.sol b/contracts/adapters/interfaces/IWETH.sol new file mode 100644 index 00000000..9d25abe4 --- /dev/null +++ b/contracts/adapters/interfaces/IWETH.sol @@ -0,0 +1,5 @@ +pragma solidity 0.8.11; + +interface IWETH { + function withdraw(uint wad) external virtual; +} diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js new file mode 100644 index 00000000..f54ef032 --- /dev/null +++ b/testUnderForked/swapAdapter.js @@ -0,0 +1,19 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +const TruffleAssert = require("truffle-assertions"); +const Ethers = require("ethers"); + +const Helpers = require("../test/helpers"); + +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); + +contract("SwapAdapter - [depositTokensToEth]", async (accounts) => { + // Fee handler is mocked by BasicFeeHandler + // deploy bridge, ERC20Handler, NativeTokenHandler, BasicFeeHandler, SwapAdapter + // use SwapRouter, USDC, WETH, user with USDC, user with ETH from mainnet fork +}); + +contract("SwapAdapter - [depositEthToTokens]", async (accounts) => { +}); From 1695f2fe6e15797581af1e10cbd614a1ceea34a3 Mon Sep 17 00:00:00 2001 From: viatrix Date: Mon, 14 Oct 2024 23:51:09 +0300 Subject: [PATCH 02/28] WIP: tests --- testUnderForked/swapAdapter.js | 93 ++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index f54ef032..f75fb9ea 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -6,14 +6,99 @@ const Ethers = require("ethers"); const Helpers = require("../test/helpers"); -const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); +const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); +const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); +const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); +const SwapAdapterContract = artifacts.require("SwapAdapter"); -contract("SwapAdapter - [depositTokensToEth]", async (accounts) => { +contract("SwapAdapter", async (accounts) => { // Fee handler is mocked by BasicFeeHandler // deploy bridge, ERC20Handler, NativeTokenHandler, BasicFeeHandler, SwapAdapter // use SwapRouter, USDC, WETH, user with USDC, user with ETH from mainnet fork -}); + const recipientAddress = accounts[2]; + const tokenAmount = Ethers.utils.parseEther("1"); + const fee = Ethers.utils.parseEther("0.05"); + const depositorAddress = accounts[1]; + const emptySetResourceData = "0x"; + const originDomainID = 1; + const destinationDomainID = 3; + const gasUsed = 100000; + const gasPrice = 200000000000; + const sender = accounts[0]; + const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; + const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + const USDC_OWNER_ADDRESS = "0x7713974908Be4BEd47172370115e8b1219F4A5f0"; + const UNISWAP_SWAP_ROUTER_ADDRESS = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; + const resourceID_USDC = Helpers.createResourceID( + USDC_ADDRESS, + originDomainID + ); + const resourceID_Native = "0x0000000000000000000000000000000000000000000000000000000000000650"; + + + let usdc_owner; + let eth_owner; + + const addressEth = "0x604981db0C06Ea1b37495265EDa4619c8Eb95A3D"; + + + const addressDai = "0xe5f8086dac91e039b1400febf0ab33ba3487f29a"; + + + const addressUsdc = "0x7713974908Be4BEd47172370115e8b1219F4A5f0"; + -contract("SwapAdapter - [depositEthToTokens]", async (accounts) => { + let BridgeInstance; + let DefaultMessageReceiverInstance; + let BasicFeeHandlerInstance; + let ERC20HandlerInstance; + let NativeTokenHandlerInstance; + let SwapAdapterInstance; + let depositData; + + beforeEach(async () => { + const provider = new Ethers.providers.JsonRpcProvider(); + usdc_owner = await provider.getSigner(USDC_OWNER_ADDRESS); + buyerForEth = await provider.getSigner(addressEth); + buyerForDai = await provider.getSigner(addressDai); + buyerForUsdc = await provider.getSigner(addressUsdc); + BridgeInstance = await Helpers.deployBridge( + originDomainID, + accounts[0] + ); + DefaultMessageReceiverInstance = await DefaultMessageReceiverContract.new([], 100000); + ERC20HandlerInstance = await ERC20HandlerContract.new( + BridgeInstance.address, + DefaultMessageReceiverInstance.address + ); + FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( + BridgeInstance.address + ); + BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( + BridgeInstance.address, + FeeHandlerRouterInstance.address + ); + NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( + BridgeInstance.address, + NativeTokenAdapter, //to deploy + DefaultMessageReceiverInstance.address + ); + SwapAdapterInstance = await SwapAdapterContract.new( + BridgeInstance.address, + resourceID_Native, + WETH_ADDRESS, + UNISWAP_SWAP_ROUTER_ADDRESS + ); + }); + + it.only("should swap tokens to ETH and bridge ETH", async () => { + + + }); + + it("should swap ETH to tokens and bridge tokens", async () => { + }); }); + From cadc696ea0d205cae9b758e3096fa8d84738740f Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 15 Oct 2024 15:04:48 +0300 Subject: [PATCH 03/28] Add use of NativeTokenAdapter --- contracts/adapters/SwapAdapter.sol | 39 +++---------------- .../interfaces/INativeTokenAdapter.sol | 2 +- contracts/adapters/interfaces/IWETH.sol | 2 +- 3 files changed, 8 insertions(+), 35 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 77ab10d7..14238e15 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -19,6 +19,7 @@ contract SwapAdapter is AccessControl { bytes32 public immutable _resourceIDEth; address immutable _weth; IV3SwapRouter public _swapRouter; + INativeTokenAdapter _nativeTokenAdapter; mapping(address => bytes32) public tokenToResourceID; @@ -37,12 +38,14 @@ contract SwapAdapter is AccessControl { IBridge bridge, bytes32 resourceIDEth, address weth, - IV3SwapRouter swapRouter + IV3SwapRouter swapRouter, + INativeTokenAdapter nativeTokenAdapter ) { _bridge = bridge; _resourceIDEth = resourceIDEth; _weth = weth; _swapRouter = swapRouter; + _nativeTokenAdapter = nativeTokenAdapter; _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -61,7 +64,6 @@ contract SwapAdapter is AccessControl { function depositTokensToEth( uint8 destinationDomainID, address recipient, - uint256 gas, address token, uint256 tokenAmount, uint256 amountOutMinimum, @@ -97,41 +99,12 @@ contract SwapAdapter is AccessControl { // Make Native Token deposit if (amount == 0) revert InsufficientAmount(amount); - bytes memory depositDataAfterAmount = abi.encodePacked( - uint256(20), - recipient - ); - uint256 fee; - { - address feeHandlerRouter = _bridge._feeHandler(); - (fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( - address(this), - _bridge._domainID(), - destinationDomainID, - _resourceIDEth, - abi.encodePacked(amount, depositDataAfterAmount), - "" // feeData - not parsed - ); - } - - if (amount < fee) revert AmountLowerThanFee(amount); - amount -= fee; - - bytes memory depositData = abi.encodePacked( - amount, - depositDataAfterAmount - ); - - _bridge.deposit{value: fee}(destinationDomainID, _resourceIDEth, depositData, ""); - - address nativeHandlerAddress = _bridge._resourceIDToHandlerAddress(_resourceIDEth); - (bool success, ) = nativeHandlerAddress.call{value: amount}(""); - if (!success) revert FailedFundsTransfer(); + _nativeTokenAdapter.depositToEVM{value: amount}(destinationDomainID, recipient); // Return unspent fee to msg.sender uint256 leftover = address(this).balance; if (leftover > 0) { - (success, ) = payable(msg.sender).call{value: leftover}(""); + payable(msg.sender).call{value: leftover}(""); // Do not revert if sender does not want to receive. } } diff --git a/contracts/adapters/interfaces/INativeTokenAdapter.sol b/contracts/adapters/interfaces/INativeTokenAdapter.sol index 3a52a13f..069576bd 100644 --- a/contracts/adapters/interfaces/INativeTokenAdapter.sol +++ b/contracts/adapters/interfaces/INativeTokenAdapter.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.11; @title Interface for Bridge contract. @author ChainSafe Systems. */ -interface IGmpTransferAdapter { +interface INativeTokenAdapter { /** @notice Makes a native token deposit with an included message. @param destinationDomainID ID of destination chain. diff --git a/contracts/adapters/interfaces/IWETH.sol b/contracts/adapters/interfaces/IWETH.sol index 9d25abe4..2ea51f40 100644 --- a/contracts/adapters/interfaces/IWETH.sol +++ b/contracts/adapters/interfaces/IWETH.sol @@ -1,5 +1,5 @@ pragma solidity 0.8.11; interface IWETH { - function withdraw(uint wad) external virtual; + function withdraw(uint wad) external; } From 72f9472cd7d8caca24f21111e541edd5628bda41 Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 15 Oct 2024 15:44:39 +0300 Subject: [PATCH 04/28] Add NativeTokenAdapter to test --- contracts/adapters/SwapAdapter.sol | 3 --- testUnderForked/swapAdapter.js | 11 +++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 14238e15..0d167ead 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -16,7 +16,6 @@ contract SwapAdapter is AccessControl { using SafeERC20 for IERC20; IBridge public immutable _bridge; - bytes32 public immutable _resourceIDEth; address immutable _weth; IV3SwapRouter public _swapRouter; INativeTokenAdapter _nativeTokenAdapter; @@ -42,7 +41,6 @@ contract SwapAdapter is AccessControl { INativeTokenAdapter nativeTokenAdapter ) { _bridge = bridge; - _resourceIDEth = resourceIDEth; _weth = weth; _swapRouter = swapRouter; _nativeTokenAdapter = nativeTokenAdapter; @@ -139,7 +137,6 @@ contract SwapAdapter is AccessControl { ); } - if (msg.value < fee) revert MsgValueLowerThanFee(msg.value); uint256 amountOut; { diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index f75fb9ea..f81afc81 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -10,6 +10,7 @@ const ERC20HandlerContract = artifacts.require("ERC20Handler"); const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); +const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); const SwapAdapterContract = artifacts.require("SwapAdapter"); @@ -54,6 +55,7 @@ contract("SwapAdapter", async (accounts) => { let DefaultMessageReceiverInstance; let BasicFeeHandlerInstance; let ERC20HandlerInstance; + let NativeTokenAdapterInstance; let NativeTokenHandlerInstance; let SwapAdapterInstance; let depositData; @@ -80,16 +82,21 @@ contract("SwapAdapter", async (accounts) => { BridgeInstance.address, FeeHandlerRouterInstance.address ); + NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( + BridgeInstance.address, + resourceID_Native + ); NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( BridgeInstance.address, - NativeTokenAdapter, //to deploy + NativeTokenAdapterInstance.address, DefaultMessageReceiverInstance.address ); SwapAdapterInstance = await SwapAdapterContract.new( BridgeInstance.address, resourceID_Native, WETH_ADDRESS, - UNISWAP_SWAP_ROUTER_ADDRESS + UNISWAP_SWAP_ROUTER_ADDRESS, + NativeTokenAdapterInstance.address ); }); From e49b23bea4743efcaba22186de83565f479a8cb7 Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 15 Oct 2024 22:41:42 +0300 Subject: [PATCH 05/28] WIP: test for swapAdapter --- testUnderForked/swapAdapter.js | 41 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index f81afc81..2888d101 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -13,6 +13,7 @@ const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); const SwapAdapterContract = artifacts.require("SwapAdapter"); +const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); contract("SwapAdapter", async (accounts) => { // Fee handler is mocked by BasicFeeHandler @@ -37,16 +38,6 @@ contract("SwapAdapter", async (accounts) => { originDomainID ); const resourceID_Native = "0x0000000000000000000000000000000000000000000000000000000000000650"; - - - let usdc_owner; - let eth_owner; - - const addressEth = "0x604981db0C06Ea1b37495265EDa4619c8Eb95A3D"; - - - const addressDai = "0xe5f8086dac91e039b1400febf0ab33ba3487f29a"; - const addressUsdc = "0x7713974908Be4BEd47172370115e8b1219F4A5f0"; @@ -58,14 +49,13 @@ contract("SwapAdapter", async (accounts) => { let NativeTokenAdapterInstance; let NativeTokenHandlerInstance; let SwapAdapterInstance; - let depositData; + let usdc; + let usdcOwner; beforeEach(async () => { const provider = new Ethers.providers.JsonRpcProvider(); - usdc_owner = await provider.getSigner(USDC_OWNER_ADDRESS); - buyerForEth = await provider.getSigner(addressEth); - buyerForDai = await provider.getSigner(addressDai); - buyerForUsdc = await provider.getSigner(addressUsdc); + usdcOwner = await provider.getSigner(USDC_OWNER_ADDRESS); + BridgeInstance = await Helpers.deployBridge( originDomainID, accounts[0] @@ -98,10 +88,29 @@ contract("SwapAdapter", async (accounts) => { UNISWAP_SWAP_ROUTER_ADDRESS, NativeTokenAdapterInstance.address ); + usdc = await ERC20MintableContract.at(USDC_ADDRESS); + + // TODO: Set resourceIds }); it.only("should swap tokens to ETH and bridge ETH", async () => { - + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathFees = [500, 500]; + const amount = 1000000; + const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + // TODO: impersonate account + await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + const tx = await SwapAdapterInstance.depositTokensToEth( + destinationDomainID, + recipientAddress.address, + USDC_ADDRESS, + amount, + amountOutMinimum, + pathTokens, + pathFees, + {from: USDC_OWNER_ADDRESS} + ); }); From a1c4da1906439b2492c4c7474af3e9bc4090858e Mon Sep 17 00:00:00 2001 From: viatrix Date: Wed, 16 Oct 2024 08:56:25 +0300 Subject: [PATCH 06/28] WIP: tests --- Makefile | 2 +- testUnderForked/swapAdapter.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 0c6358fb..e102360b 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ start-ganache: start-forkedMainnet: @echo " > \033[32mStarting forked environment... \033[0m " - ganache -f $(FORKED_TESTS_PROVIDER) & sleep 3 + ganache -f $(FORKED_TESTS_PROVIDER) -u "0x7713974908Be4BEd47172370115e8b1219F4A5f0" & sleep 3 test-forked: @echo " > \033[32mTesting contracts... \033[0m " diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 2888d101..698c9883 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -97,19 +97,21 @@ contract("SwapAdapter", async (accounts) => { const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; const pathFees = [500, 500]; const amount = 1000000; + const fee = 0; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); // TODO: impersonate account await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); const tx = await SwapAdapterInstance.depositTokensToEth( destinationDomainID, - recipientAddress.address, + recipientAddress, USDC_ADDRESS, amount, amountOutMinimum, pathTokens, pathFees, - {from: USDC_OWNER_ADDRESS} + {from: USDC_OWNER_ADDRESS, + value: fee} ); }); From 4c73a068d9e0b1923d79ae5440a63c57ecf94fa6 Mon Sep 17 00:00:00 2001 From: viatrix Date: Thu, 17 Oct 2024 22:07:17 +0300 Subject: [PATCH 07/28] Add success test for token-eth swap --- contracts/adapters/SwapAdapter.sol | 12 ++++-- testUnderForked/swapAdapter.js | 61 ++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 0d167ead..65eed57a 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -32,6 +32,7 @@ contract SwapAdapter is AccessControl { error AmountLowerThanFee(uint256 amount); event TokenResourceIDSet(address token, bytes32 resourceID); + event TokensSwapped(address token, uint256 amountOut); constructor( IBridge bridge, @@ -67,7 +68,7 @@ contract SwapAdapter is AccessControl { uint256 amountOutMinimum, address[] memory pathTokens, uint24[] memory pathFees - ) external payable { + ) external { if (tokenToResourceID[token] == bytes32(0)) revert TokenInvalid(); // Swap all tokens to ETH (exact input) @@ -93,6 +94,8 @@ contract SwapAdapter is AccessControl { amount = _swapRouter.exactInput(params); } + emit TokensSwapped(_weth, amount); + IWETH(_weth).withdraw(amount); // Make Native Token deposit @@ -158,6 +161,7 @@ contract SwapAdapter is AccessControl { }); amountOut = _swapRouter.exactInput{value: swapAmount}(params); + emit TokensSwapped(token, amountOut); } bytes memory depositData = abi.encodePacked( @@ -188,14 +192,16 @@ contract SwapAdapter is AccessControl { } tokenIn = tokenIn == address(0) ? address(_weth) : tokenIn; - if (tokens[tokens.length - 1] != tokenIn) revert PathInvalid(); + if (tokens[0] != tokenIn) revert PathInvalid(); tokenOut = tokenOut == address(0) ? address(_weth) : tokenOut; - if (tokens[0] != tokenOut) revert PathInvalid(); + if (tokens[tokens.length - 1] != tokenOut) revert PathInvalid(); for (uint256 i = 0; i < tokens.length - 1; i++){ path = abi.encodePacked(path, tokens[i], fees[i]); } path = abi.encodePacked(path, tokens[tokens.length - 1]); } + + receive() external payable {} } diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 698c9883..0e8d6e28 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -5,6 +5,7 @@ const TruffleAssert = require("truffle-assertions"); const Ethers = require("ethers"); const Helpers = require("../test/helpers"); +const { provider } = require("ganache"); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); @@ -20,8 +21,7 @@ contract("SwapAdapter", async (accounts) => { // deploy bridge, ERC20Handler, NativeTokenHandler, BasicFeeHandler, SwapAdapter // use SwapRouter, USDC, WETH, user with USDC, user with ETH from mainnet fork const recipientAddress = accounts[2]; - const tokenAmount = Ethers.utils.parseEther("1"); - const fee = Ethers.utils.parseEther("0.05"); + const fee = 1000; const depositorAddress = accounts[1]; const emptySetResourceData = "0x"; const originDomainID = 1; @@ -50,6 +50,7 @@ contract("SwapAdapter", async (accounts) => { let NativeTokenHandlerInstance; let SwapAdapterInstance; let usdc; + let weth; let usdcOwner; beforeEach(async () => { @@ -89,20 +90,36 @@ contract("SwapAdapter", async (accounts) => { NativeTokenAdapterInstance.address ); usdc = await ERC20MintableContract.at(USDC_ADDRESS); + weth = await ERC20MintableContract.at(WETH_ADDRESS); + + await BridgeInstance.adminSetResource( + NativeTokenHandlerInstance.address, + resourceID_Native, + NativeTokenHandlerInstance.address, + emptySetResourceData + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID_Native, fee); + await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID_Native, + BasicFeeHandlerInstance.address + ), + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); - // TODO: Set resourceIds }); it.only("should swap tokens to ETH and bridge ETH", async () => { const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; - const pathFees = [500, 500]; + const pathFees = [500]; const amount = 1000000; - const fee = 0; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); // TODO: impersonate account await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); - const tx = await SwapAdapterInstance.depositTokensToEth( + const depositTx = await SwapAdapterInstance.depositTokensToEth( destinationDomainID, recipientAddress, USDC_ADDRESS, @@ -110,10 +127,38 @@ contract("SwapAdapter", async (accounts) => { amountOutMinimum, pathTokens, pathFees, - {from: USDC_OWNER_ADDRESS, - value: fee} + {from: USDC_OWNER_ADDRESS} ); + expect(await web3.eth.getBalance(SwapAdapterInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(BasicFeeHandlerInstance.address)).to.eq(fee.toString()); + expect(await web3.eth.getBalance(NativeTokenHandlerInstance.address)).to.not.eq("0"); + const depositCount = await BridgeInstance._depositCounts.call( + destinationDomainID + ); + const expectedDepositNonce = 1; + assert.strictEqual(depositCount.toNumber(), expectedDepositNonce); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const amountOut = events[events.length - 1].args.amountOut; + + const depositData = await Helpers.createERCDepositData(amountOut - fee, 20, recipientAddress); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID_Native.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === NativeTokenAdapterInstance.address && + event.data === depositData.toLowerCase() && + event.handlerResponse === null + ); + }); }); it("should swap ETH to tokens and bridge tokens", async () => { From 202ec3f0213dc0be27b59f6f0f8007f6d81eda4a Mon Sep 17 00:00:00 2001 From: viatrix Date: Fri, 18 Oct 2024 16:54:35 +0300 Subject: [PATCH 08/28] Add success test for swap eth to tokens --- testUnderForked/swapAdapter.js | 69 +++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 0e8d6e28..21ad14bc 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -98,7 +98,16 @@ contract("SwapAdapter", async (accounts) => { NativeTokenHandlerInstance.address, emptySetResourceData ); + + await BridgeInstance.adminSetResource( + ERC20HandlerInstance.address, + resourceID_USDC, + USDC_ADDRESS, + emptySetResourceData + ); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID_Native, fee); + await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID_USDC, fee); await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), await FeeHandlerRouterInstance.adminSetResourceHandler( destinationDomainID, @@ -106,18 +115,23 @@ contract("SwapAdapter", async (accounts) => { BasicFeeHandlerInstance.address ), + await FeeHandlerRouterInstance.adminSetResourceHandler( + destinationDomainID, + resourceID_USDC, + BasicFeeHandlerInstance.address + ), + // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); }); - it.only("should swap tokens to ETH and bridge ETH", async () => { + it("should swap tokens to ETH and bridge ETH", async () => { const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; const pathFees = [500]; const amount = 1000000; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); - // TODO: impersonate account await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); const depositTx = await SwapAdapterInstance.depositTokensToEth( destinationDomainID, @@ -162,6 +176,57 @@ contract("SwapAdapter", async (accounts) => { }); it("should swap ETH to tokens and bridge tokens", async () => { + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathFees = [500]; + const amount = Ethers.utils.parseEther("1"); + const amountOutMinimum = 2000000000; + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + const depositTx = await SwapAdapterInstance.depositEthToTokens( + destinationDomainID, + recipientAddress, + USDC_ADDRESS, + amountOutMinimum, + pathTokens, + pathFees, + { + value: amount, + from: depositorAddress + } + ); + expect((await usdc.balanceOf(SwapAdapterInstance.address)).toString()).to.eq("0"); + expect(await web3.eth.getBalance(SwapAdapterInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(BridgeInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(FeeHandlerRouterInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(BasicFeeHandlerInstance.address)).to.eq(fee.toString()); + expect(await usdc.balanceOf(ERC20HandlerInstance.address)).to.not.eq("0"); + + const depositCount = await BridgeInstance._depositCounts.call( + destinationDomainID + ); + const expectedDepositNonce = 1; + assert.strictEqual(depositCount.toNumber(), expectedDepositNonce); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const amountOut = events[events.length - 1].args.amountOut; + expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); + + const depositData = await Helpers.createERCDepositData(amountOut.toNumber(), 20, recipientAddress); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID_USDC.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === SwapAdapterInstance.address && + event.data === depositData.toLowerCase() && + event.handlerResponse === null + ); + }); }); }); From dbe2c20507251db41c0acb3ed95f3d1eababd0de Mon Sep 17 00:00:00 2001 From: viatrix Date: Fri, 18 Oct 2024 18:16:24 +0300 Subject: [PATCH 09/28] Update SwapAdapter --- contracts/adapters/SwapAdapter.sol | 5 ++--- testUnderForked/swapAdapter.js | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 65eed57a..0d07f0f6 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -36,7 +36,6 @@ contract SwapAdapter is AccessControl { constructor( IBridge bridge, - bytes32 resourceIDEth, address weth, IV3SwapRouter swapRouter, INativeTokenAdapter nativeTokenAdapter @@ -94,12 +93,12 @@ contract SwapAdapter is AccessControl { amount = _swapRouter.exactInput(params); } - emit TokensSwapped(_weth, amount); - IWETH(_weth).withdraw(amount); // Make Native Token deposit if (amount == 0) revert InsufficientAmount(amount); + emit TokensSwapped(_weth, amount); + _nativeTokenAdapter.depositToEVM{value: amount}(destinationDomainID, recipient); // Return unspent fee to msg.sender diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 21ad14bc..0763b0bd 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -84,7 +84,6 @@ contract("SwapAdapter", async (accounts) => { ); SwapAdapterInstance = await SwapAdapterContract.new( BridgeInstance.address, - resourceID_Native, WETH_ADDRESS, UNISWAP_SWAP_ROUTER_ADDRESS, NativeTokenAdapterInstance.address From 5c6667609cf3c5e1d3ed269c00210556dd3da24a Mon Sep 17 00:00:00 2001 From: viatrix Date: Fri, 18 Oct 2024 19:05:28 +0300 Subject: [PATCH 10/28] Update SwapAdapter --- contracts/adapters/SwapAdapter.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 0d07f0f6..17d66082 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -93,12 +93,12 @@ contract SwapAdapter is AccessControl { amount = _swapRouter.exactInput(params); } + if (amount == 0) revert InsufficientAmount(amount); + + emit TokensSwapped(_weth, amount); IWETH(_weth).withdraw(amount); // Make Native Token deposit - if (amount == 0) revert InsufficientAmount(amount); - emit TokensSwapped(_weth, amount); - _nativeTokenAdapter.depositToEVM{value: amount}(destinationDomainID, recipient); // Return unspent fee to msg.sender From 465e8951226d148f138b15012b6b4b5fd4e3e591 Mon Sep 17 00:00:00 2001 From: andersonlee725 Date: Tue, 22 Oct 2024 22:29:09 +0800 Subject: [PATCH 11/28] add more tests --- testUnderForked/swapAdapter.js | 117 ++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 0763b0bd..024a06f8 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -227,5 +227,120 @@ contract("SwapAdapter", async (accounts) => { ); }); }); -}); + it("should fail if no approve", async () => { + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathFees = [500]; + const amount = 1000000; + const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + await Helpers.reverts( + SwapAdapterInstance.depositTokensToEth( + destinationDomainID, + recipientAddress, + USDC_ADDRESS, + amount, + amountOutMinimum, + pathTokens, + pathFees, + {from: USDC_OWNER_ADDRESS} + ) + ); + }); + + it("should fail if invalid path [tokens length and fees length]", async () => { + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathFees = [500, 300]; + const amount = 1000000; + const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + const errorValues = await Helpers.expectToRevertWithCustomError( + SwapAdapterInstance.depositTokensToEth( + destinationDomainID, + recipientAddress, + USDC_ADDRESS, + amount, + amountOutMinimum, + pathTokens, + pathFees, + {from: USDC_OWNER_ADDRESS} + ), + "PathInvalid()" + ); + // await Helpers.reverts( + // SwapAdapterInstance.depositTokensToEth( + // destinationDomainID, + // recipientAddress, + // USDC_ADDRESS, + // amount, + // amountOutMinimum, + // pathTokens, + // pathFees, + // {from: USDC_OWNER_ADDRESS} + // ) + // ); + }); + + it("should fail if invalid path [tokenIn is not token0]", async () => { + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathFees = [500]; + const amount = 1000000; + const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + await Helpers.reverts( + SwapAdapterInstance.depositTokensToEth( + destinationDomainID, + recipientAddress, + WETH_ADDRESS, + amount, + amountOutMinimum, + pathTokens, + pathFees, + {from: USDC_OWNER_ADDRESS} + ) + ); + }); + + it("should fail if invalid path [tokenOut is not weth]", async () => { + const pathTokens = [USDC_ADDRESS, USDC_ADDRESS]; + const pathFees = [500]; + const amount = 1000000; + const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + await Helpers.reverts( + SwapAdapterInstance.depositTokensToEth( + destinationDomainID, + recipientAddress, + USDC_ADDRESS, + amount, + amountOutMinimum, + pathTokens, + pathFees, + {from: USDC_OWNER_ADDRESS} + ) + ); + }); + + it("should fail if resource id is not configured", async () => { + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathFees = [500]; + const amount = 1000000; + const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + await Helpers.reverts( + SwapAdapterInstance.depositTokensToEth( + destinationDomainID, + recipientAddress, + USDC_ADDRESS, + amount, + amountOutMinimum, + pathTokens, + pathFees, + {from: USDC_OWNER_ADDRESS} + ) + ); + }); +}); From 154f9f76f7d3d58fbcdcb6ebe4279c23df5aa371 Mon Sep 17 00:00:00 2001 From: viatrix Date: Wed, 23 Oct 2024 16:18:35 +0300 Subject: [PATCH 12/28] Update tests --- .env.sample | 1 + Makefile | 7 +++- testUnderForked/swapAdapter.js | 60 +++++++++++++--------------------- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/.env.sample b/.env.sample index 57cd4957..b6126542 100644 --- a/.env.sample +++ b/.env.sample @@ -4,3 +4,4 @@ GOERLI_API_KEY="provider api key" MUMBAI_MNEMONIC="your mumbai mnemonic" MUMBAI_PROVIDER_URL="provider url" MUMBAI_API_KEY="provider api key" +USDC_OWNER_ADDRESS=0x7713974908Be4BEd47172370115e8b1219F4A5f0 diff --git a/Makefile b/Makefile index e102360b..2570cff9 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +ifneq (,$(wildcard ./.env)) + include .env + export +endif + URL?=http://localhost:8545 install-deps: @@ -19,7 +24,7 @@ start-ganache: start-forkedMainnet: @echo " > \033[32mStarting forked environment... \033[0m " - ganache -f $(FORKED_TESTS_PROVIDER) -u "0x7713974908Be4BEd47172370115e8b1219F4A5f0" & sleep 3 + ganache -f $(FORKED_TESTS_PROVIDER) -u ${USDC_OWNER_ADDRESS} & sleep 3 test-forked: @echo " > \033[32mTesting contracts... \033[0m " diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 024a06f8..778b14de 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -6,6 +6,8 @@ const Ethers = require("ethers"); const Helpers = require("../test/helpers"); const { provider } = require("ganache"); +const dotenv = require("dotenv"); +dotenv.config(); const ERC20HandlerContract = artifacts.require("ERC20Handler"); const DefaultMessageReceiverContract = artifacts.require("DefaultMessageReceiver"); @@ -26,12 +28,12 @@ contract("SwapAdapter", async (accounts) => { const emptySetResourceData = "0x"; const originDomainID = 1; const destinationDomainID = 3; - const gasUsed = 100000; - const gasPrice = 200000000000; - const sender = accounts[0]; const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - const USDC_OWNER_ADDRESS = "0x7713974908Be4BEd47172370115e8b1219F4A5f0"; + const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; + // const USDC_OWNER_ADDRESS = "0x7713974908Be4BEd47172370115e8b1219F4A5f0"; + // console.log(USDC_OWNER_ADDRESS); + // console.log(process.env.USDC_OWNER_ADDRESS); const UNISWAP_SWAP_ROUTER_ADDRESS = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; const resourceID_USDC = Helpers.createResourceID( USDC_ADDRESS, @@ -39,9 +41,6 @@ contract("SwapAdapter", async (accounts) => { ); const resourceID_Native = "0x0000000000000000000000000000000000000000000000000000000000000650"; - const addressUsdc = "0x7713974908Be4BEd47172370115e8b1219F4A5f0"; - - let BridgeInstance; let DefaultMessageReceiverInstance; let BasicFeeHandlerInstance; @@ -50,13 +49,8 @@ contract("SwapAdapter", async (accounts) => { let NativeTokenHandlerInstance; let SwapAdapterInstance; let usdc; - let weth; - let usdcOwner; beforeEach(async () => { - const provider = new Ethers.providers.JsonRpcProvider(); - usdcOwner = await provider.getSigner(USDC_OWNER_ADDRESS); - BridgeInstance = await Helpers.deployBridge( originDomainID, accounts[0] @@ -89,7 +83,6 @@ contract("SwapAdapter", async (accounts) => { NativeTokenAdapterInstance.address ); usdc = await ERC20MintableContract.at(USDC_ADDRESS); - weth = await ERC20MintableContract.at(WETH_ADDRESS); await BridgeInstance.adminSetResource( NativeTokenHandlerInstance.address, @@ -255,8 +248,8 @@ contract("SwapAdapter", async (accounts) => { const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); - const errorValues = await Helpers.expectToRevertWithCustomError( - SwapAdapterInstance.depositTokensToEth( + await Helpers.expectToRevertWithCustomError( + SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, USDC_ADDRESS, @@ -268,38 +261,27 @@ contract("SwapAdapter", async (accounts) => { ), "PathInvalid()" ); - // await Helpers.reverts( - // SwapAdapterInstance.depositTokensToEth( - // destinationDomainID, - // recipientAddress, - // USDC_ADDRESS, - // amount, - // amountOutMinimum, - // pathTokens, - // pathFees, - // {from: USDC_OWNER_ADDRESS} - // ) - // ); }); it("should fail if invalid path [tokenIn is not token0]", async () => { - const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; const amount = 1000000; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); - await Helpers.reverts( - SwapAdapterInstance.depositTokensToEth( + await Helpers.expectToRevertWithCustomError( + SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, - WETH_ADDRESS, + USDC_ADDRESS, amount, amountOutMinimum, pathTokens, pathFees, {from: USDC_OWNER_ADDRESS} - ) + ), + "PathInvalid()" ); }); @@ -310,8 +292,8 @@ contract("SwapAdapter", async (accounts) => { const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); - await Helpers.reverts( - SwapAdapterInstance.depositTokensToEth( + await Helpers.expectToRevertWithCustomError( + SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, USDC_ADDRESS, @@ -320,7 +302,8 @@ contract("SwapAdapter", async (accounts) => { pathTokens, pathFees, {from: USDC_OWNER_ADDRESS} - ) + ), + "PathInvalid()" ); }); @@ -330,8 +313,8 @@ contract("SwapAdapter", async (accounts) => { const amount = 1000000; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); - await Helpers.reverts( - SwapAdapterInstance.depositTokensToEth( + await Helpers.expectToRevertWithCustomError( + SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, USDC_ADDRESS, @@ -340,7 +323,8 @@ contract("SwapAdapter", async (accounts) => { pathTokens, pathFees, {from: USDC_OWNER_ADDRESS} - ) + ), + "TokenInvalid()" ); }); }); From abe40cfe6781987663ddec5442bb41a1be40425c Mon Sep 17 00:00:00 2001 From: viatrix Date: Wed, 23 Oct 2024 16:35:23 +0300 Subject: [PATCH 13/28] Add natspec --- contracts/adapters/SwapAdapter.sol | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 17d66082..61d019b0 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -11,6 +11,11 @@ import "../../contracts/adapters/interfaces/INativeTokenAdapter.sol"; import "../../contracts/adapters/interfaces/IWETH.sol"; import "../../contracts/adapters/interfaces/IV3SwapRouter.sol"; +/** + @title Contract that swaps tokens to ETH or ETH to tokens using Uniswap + and then makes a deposit to the Bridge. + @author ChainSafe Systems. + */ contract SwapAdapter is AccessControl { using SafeERC20 for IERC20; @@ -59,6 +64,16 @@ contract SwapAdapter is AccessControl { emit TokenResourceIDSet(token, resourceID); } + /** + @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. + @param destinationDomainID ID of chain deposit will be bridged to. + @param recipient Recipient of the deposit. + @param token Input token to be swapped. + @param tokenAmount Amount of tokens to be swapped. + @param amountOutMinimum Minimal amount of ETH to be accepted as a swap output. + @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. + @param pathFees Fees for Uniswap pools. + */ function depositTokensToEth( uint8 destinationDomainID, address recipient, @@ -109,6 +124,15 @@ contract SwapAdapter is AccessControl { } } + /** + @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. + @param destinationDomainID ID of chain deposit will be bridged to. + @param recipient Recipient of the deposit. + @param token Output token to be deposited after swapping. + @param amountOutMinimum Minimal amount of tokens to be accepted as a swap output. + @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. + @param pathFees Fees for Uniswap pools. + */ function depositEthToTokens( uint8 destinationDomainID, address recipient, From e46a7f34fdaab91701dbbcec78fb2aef2dc4b6b5 Mon Sep 17 00:00:00 2001 From: viatrix Date: Thu, 24 Oct 2024 23:23:09 +0300 Subject: [PATCH 14/28] Add optional contract call --- contracts/adapters/SwapAdapter.sol | 140 +++++++++++------- .../interfaces/INativeTokenAdapter.sol | 11 +- testUnderForked/swapAdapter.js | 110 ++++++++++++-- 3 files changed, 188 insertions(+), 73 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 61d019b0..a5aacbf8 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -27,6 +27,21 @@ contract SwapAdapter is AccessControl { mapping(address => bytes32) public tokenToResourceID; + // Used to avoid "stack too deep" error + struct LocalVars { + bytes32 resourceID; + bytes depositDataAfterAmount; + uint256 fee; + address feeHandlerRouter; + uint256 amountOut; + uint256 swapAmount; + bytes path; + IV3SwapRouter.ExactInputParams params; + bytes depositData; + address ERC20HandlerAddress; + uint256 leftover; + } + error CallerNotAdmin(); error AlreadySet(); error TokenInvalid(); @@ -68,8 +83,14 @@ contract SwapAdapter is AccessControl { @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. - @param token Input token to be swapped. - @param tokenAmount Amount of tokens to be swapped. + @param gas The amount of gas needed to successfully execute the call to recipient on the destination. Fee amount is + directly affected by this value. + @param message Arbitrary encoded bytes array that will be passed as the third argument in the + ISygmaMessageReceiver(recipient).handleSygmaMessage(_, _, message) call. If you intend to use the + DefaultMessageReceiver, make sure to encode the message to comply with the + DefaultMessageReceiver.handleSygmaMessage() message decoding implementation. + @param token Input token to be swapped. + @param tokenAmount Amount of tokens to be swapped. @param amountOutMinimum Minimal amount of ETH to be accepted as a swap output. @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. @param pathFees Fees for Uniswap pools. @@ -77,6 +98,8 @@ contract SwapAdapter is AccessControl { function depositTokensToEth( uint8 destinationDomainID, address recipient, + uint256 gas, + bytes calldata message, address token, uint256 tokenAmount, uint256 amountOutMinimum, @@ -108,13 +131,16 @@ contract SwapAdapter is AccessControl { amount = _swapRouter.exactInput(params); } - if (amount == 0) revert InsufficientAmount(amount); - emit TokensSwapped(_weth, amount); IWETH(_weth).withdraw(amount); // Make Native Token deposit - _nativeTokenAdapter.depositToEVM{value: amount}(destinationDomainID, recipient); + _nativeTokenAdapter.depositToEVMWithMessage{value: amount}( + destinationDomainID, + recipient, + gas, + message + ); // Return unspent fee to msg.sender uint256 leftover = address(this).balance; @@ -128,7 +154,13 @@ contract SwapAdapter is AccessControl { @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. - @param token Output token to be deposited after swapping. + @param gas The amount of gas needed to successfully execute the call to recipient on the destination. Fee amount is + directly affected by this value. + @param message Arbitrary encoded bytes array that will be passed as the third argument in the + ISygmaMessageReceiver(recipient).handleSygmaMessage(_, _, message) call. If you intend to use the + DefaultMessageReceiver, make sure to encode the message to comply with the + DefaultMessageReceiver.handleSygmaMessage() message decoding implementation. + @param token Output token to be deposited after swapping. @param amountOutMinimum Minimal amount of tokens to be accepted as a swap output. @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. @param pathFees Fees for Uniswap pools. @@ -136,70 +168,70 @@ contract SwapAdapter is AccessControl { function depositEthToTokens( uint8 destinationDomainID, address recipient, + uint256 gas, + bytes calldata message, address token, uint256 amountOutMinimum, address[] memory pathTokens, uint24[] memory pathFees ) external payable { - bytes32 resourceID = tokenToResourceID[token]; - if (resourceID == bytes32(0)) revert TokenInvalid(); + LocalVars memory vars; + vars.resourceID = tokenToResourceID[token]; + if (vars.resourceID == bytes32(0)) revert TokenInvalid(); // Compose depositData - bytes memory depositDataAfterAmount = abi.encodePacked( + vars.depositDataAfterAmount = abi.encodePacked( uint256(20), - recipient + recipient, + gas, + uint256(message.length), + message ); if (msg.value == 0) revert InsufficientAmount(msg.value); - uint256 fee; - { - address feeHandlerRouter = _bridge._feeHandler(); - (fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( - address(this), - _bridge._domainID(), - destinationDomainID, - resourceID, - abi.encodePacked(msg.value, depositDataAfterAmount), - "" // feeData - not parsed - ); - } - - if (msg.value < fee) revert MsgValueLowerThanFee(msg.value); - uint256 amountOut; - { - uint256 swapAmount = msg.value - fee; - // Convert everything except the fee - - // Swap ETH to tokens (exact input) - bytes memory path = _verifyAndEncodePath( - pathTokens, - pathFees, - _weth, - token - ); - IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter.ExactInputParams({ - path: path, - recipient: address(this), - amountIn: swapAmount, - amountOutMinimum: amountOutMinimum - }); + vars.feeHandlerRouter = _bridge._feeHandler(); + (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + vars.resourceID, + abi.encodePacked(msg.value, vars.depositDataAfterAmount), + "" // feeData - not parsed + ); - amountOut = _swapRouter.exactInput{value: swapAmount}(params); - emit TokensSwapped(token, amountOut); - } + if (msg.value < vars.fee) revert MsgValueLowerThanFee(msg.value); + vars.swapAmount = msg.value - vars.fee; + // Convert everything except the fee - bytes memory depositData = abi.encodePacked( - amountOut, - depositDataAfterAmount + // Swap ETH to tokens (exact input) + vars.path = _verifyAndEncodePath( + pathTokens, + pathFees, + _weth, + token + ); + vars.params = IV3SwapRouter.ExactInputParams({ + path: vars.path, + recipient: address(this), + amountIn: vars.swapAmount, + amountOutMinimum: amountOutMinimum + }); + + vars.amountOut = _swapRouter.exactInput{value: vars.swapAmount}(vars.params); + emit TokensSwapped(token, vars.amountOut); + + vars.depositData = abi.encodePacked( + vars.amountOut, + vars.depositDataAfterAmount ); - address ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(resourceID); - IERC20(token).safeApprove(address(ERC20HandlerAddress), amountOut); - _bridge.deposit{value: fee}(destinationDomainID, resourceID, depositData, ""); + vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); + IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), vars.amountOut); + _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); // Return unspent fee to msg.sender - uint256 leftover = address(this).balance; - if (leftover > 0) { - payable(msg.sender).call{value: leftover}(""); + vars.leftover = address(this).balance; + if (vars.leftover > 0) { + payable(msg.sender).call{value: vars.leftover}(""); // Do not revert if sender does not want to receive. } } diff --git a/contracts/adapters/interfaces/INativeTokenAdapter.sol b/contracts/adapters/interfaces/INativeTokenAdapter.sol index 069576bd..baaf2672 100644 --- a/contracts/adapters/interfaces/INativeTokenAdapter.sol +++ b/contracts/adapters/interfaces/INativeTokenAdapter.sol @@ -7,14 +7,11 @@ pragma solidity 0.8.11; @author ChainSafe Systems. */ interface INativeTokenAdapter { - /** - @notice Makes a native token deposit with an included message. - @param destinationDomainID ID of destination chain. - @param recipientAddress Address that will receive native tokens on destination chain. - */ - function depositToEVM( + function depositToEVMWithMessage( uint8 destinationDomainID, - address recipientAddress + address recipient, + uint256 gas, + bytes calldata message ) external payable; } diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 778b14de..b159c5ee 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -28,27 +28,29 @@ contract("SwapAdapter", async (accounts) => { const emptySetResourceData = "0x"; const originDomainID = 1; const destinationDomainID = 3; + const executionGasAmount = 30000000; + const expectedDepositNonce = 1; const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; - // const USDC_OWNER_ADDRESS = "0x7713974908Be4BEd47172370115e8b1219F4A5f0"; - // console.log(USDC_OWNER_ADDRESS); - // console.log(process.env.USDC_OWNER_ADDRESS); const UNISWAP_SWAP_ROUTER_ADDRESS = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; const resourceID_USDC = Helpers.createResourceID( USDC_ADDRESS, originDomainID ); const resourceID_Native = "0x0000000000000000000000000000000000000000000000000000000000000650"; + const transactionId = "0x0000000000000000000000000000000000000000000000000000000000000001"; let BridgeInstance; let DefaultMessageReceiverInstance; let BasicFeeHandlerInstance; let ERC20HandlerInstance; + let ERC20MintableInstance; let NativeTokenAdapterInstance; let NativeTokenHandlerInstance; let SwapAdapterInstance; let usdc; + let message; beforeEach(async () => { BridgeInstance = await Helpers.deployBridge( @@ -60,6 +62,10 @@ contract("SwapAdapter", async (accounts) => { BridgeInstance.address, DefaultMessageReceiverInstance.address ); + ERC20MintableInstance = await ERC20MintableContract.new( + "token", + "TOK" + ); FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( BridgeInstance.address ); @@ -105,13 +111,28 @@ contract("SwapAdapter", async (accounts) => { destinationDomainID, resourceID_Native, BasicFeeHandlerInstance.address - ), + ); await FeeHandlerRouterInstance.adminSetResourceHandler( destinationDomainID, resourceID_USDC, BasicFeeHandlerInstance.address - ), + ); + + const mintableERC20Iface = new Ethers.utils.Interface(["function mint(address to, uint256 amount)"]); + const actions = [{ + nativeValue: 0, + callTo: ERC20MintableInstance.address, + approveTo: Ethers.constants.AddressZero, + tokenSend: Ethers.constants.AddressZero, + tokenReceive: Ethers.constants.AddressZero, + data: mintableERC20Iface.encodeFunctionData("mint", [recipientAddress, "20"]), + }]; + message = Helpers.createMessageCallData( + transactionId, + actions, + DefaultMessageReceiverInstance.address + ); // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); @@ -128,6 +149,8 @@ contract("SwapAdapter", async (accounts) => { const depositTx = await SwapAdapterInstance.depositTokensToEth( destinationDomainID, recipientAddress, + executionGasAmount, + message, USDC_ADDRESS, amount, amountOutMinimum, @@ -142,7 +165,6 @@ contract("SwapAdapter", async (accounts) => { const depositCount = await BridgeInstance._depositCounts.call( destinationDomainID ); - const expectedDepositNonce = 1; assert.strictEqual(depositCount.toNumber(), expectedDepositNonce); const internalTx = await TruffleAssert.createTransactionResult( @@ -153,7 +175,8 @@ contract("SwapAdapter", async (accounts) => { const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); const amountOut = events[events.length - 1].args.amountOut; - const depositData = await Helpers.createERCDepositData(amountOut - fee, 20, recipientAddress); + const depositData = await Helpers.createOptionalContractCallDepositData(amountOut - fee, recipientAddress, executionGasAmount, + message); TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { return ( @@ -176,6 +199,8 @@ contract("SwapAdapter", async (accounts) => { const depositTx = await SwapAdapterInstance.depositEthToTokens( destinationDomainID, recipientAddress, + executionGasAmount, + message, USDC_ADDRESS, amountOutMinimum, pathTokens, @@ -207,7 +232,8 @@ contract("SwapAdapter", async (accounts) => { const amountOut = events[events.length - 1].args.amountOut; expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); - const depositData = await Helpers.createERCDepositData(amountOut.toNumber(), 20, recipientAddress); + const depositData = await Helpers.createOptionalContractCallDepositData(amountOut.toNumber(), recipientAddress, executionGasAmount, + message); TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { return ( @@ -231,6 +257,8 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth( destinationDomainID, recipientAddress, + executionGasAmount, + message, USDC_ADDRESS, amount, amountOutMinimum, @@ -241,7 +269,7 @@ contract("SwapAdapter", async (accounts) => { ); }); - it("should fail if invalid path [tokens length and fees length]", async () => { + it("should fail if the path is invalid [tokens length and fees length]", async () => { const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; const pathFees = [500, 300]; const amount = 1000000; @@ -252,6 +280,8 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, + executionGasAmount, + message, USDC_ADDRESS, amount, amountOutMinimum, @@ -263,7 +293,7 @@ contract("SwapAdapter", async (accounts) => { ); }); - it("should fail if invalid path [tokenIn is not token0]", async () => { + it("should fail if the path is invalid [tokenIn is not token0]", async () => { const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; const amount = 1000000; @@ -274,6 +304,8 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, + executionGasAmount, + message, USDC_ADDRESS, amount, amountOutMinimum, @@ -285,7 +317,7 @@ contract("SwapAdapter", async (accounts) => { ); }); - it("should fail if invalid path [tokenOut is not weth]", async () => { + it("should fail if the path is invalid [tokenOut is not weth]", async () => { const pathTokens = [USDC_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; const amount = 1000000; @@ -296,6 +328,8 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, + executionGasAmount, + message, USDC_ADDRESS, amount, amountOutMinimum, @@ -307,7 +341,7 @@ contract("SwapAdapter", async (accounts) => { ); }); - it("should fail if resource id is not configured", async () => { + it("should fail if the resource id is not configured", async () => { const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; const pathFees = [500]; const amount = 1000000; @@ -317,6 +351,8 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, + executionGasAmount, + message, USDC_ADDRESS, amount, amountOutMinimum, @@ -327,4 +363,54 @@ contract("SwapAdapter", async (accounts) => { "TokenInvalid()" ); }); + + it("should fail if no msg.value supplied", async () => { + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathFees = [500]; + const amount = Ethers.utils.parseEther("1"); + const amountOutMinimum = 2000000000; + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + await Helpers.expectToRevertWithCustomError( + SwapAdapterInstance.depositEthToTokens.call( + destinationDomainID, + recipientAddress, + executionGasAmount, + message, + USDC_ADDRESS, + amountOutMinimum, + pathTokens, + pathFees, + { + value: 0, + from: depositorAddress + } + ), + "InsufficientAmount(uint256)" + ); + }); + + it("should fail if msg.value is less than fee", async () => { + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathFees = [500]; + const amount = Ethers.utils.parseEther("1"); + const amountOutMinimum = 2000000000; + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + await Helpers.expectToRevertWithCustomError( + SwapAdapterInstance.depositEthToTokens.call( + destinationDomainID, + recipientAddress, + executionGasAmount, + message, + USDC_ADDRESS, + amountOutMinimum, + pathTokens, + pathFees, + { + value: 5, + from: depositorAddress + } + ), + "MsgValueLowerThanFee(uint256)" + ); + }); }); From 4db63c94c7e88b3dd35982ccfbbd77d6755f010d Mon Sep 17 00:00:00 2001 From: viatrix Date: Fri, 25 Oct 2024 14:53:34 +0300 Subject: [PATCH 15/28] Fix lint --- testUnderForked/swapAdapter.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index b159c5ee..1f07af10 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -5,7 +5,6 @@ const TruffleAssert = require("truffle-assertions"); const Ethers = require("ethers"); const Helpers = require("../test/helpers"); -const { provider } = require("ganache"); const dotenv = require("dotenv"); dotenv.config(); @@ -172,11 +171,15 @@ contract("SwapAdapter", async (accounts) => { depositTx.tx ); - const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); const amountOut = events[events.length - 1].args.amountOut; - const depositData = await Helpers.createOptionalContractCallDepositData(amountOut - fee, recipientAddress, executionGasAmount, - message); + const depositData = await Helpers.createOptionalContractCallDepositData( + amountOut - fee, + recipientAddress, + executionGasAmount, + message + ); TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { return ( @@ -228,12 +231,16 @@ contract("SwapAdapter", async (accounts) => { depositTx.tx ); - const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); const amountOut = events[events.length - 1].args.amountOut; expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); - const depositData = await Helpers.createOptionalContractCallDepositData(amountOut.toNumber(), recipientAddress, executionGasAmount, - message); + const depositData = await Helpers.createOptionalContractCallDepositData( + amountOut.toNumber(), + recipientAddress, + executionGasAmount, + message + ); TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { return ( @@ -367,7 +374,6 @@ contract("SwapAdapter", async (accounts) => { it("should fail if no msg.value supplied", async () => { const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; - const amount = Ethers.utils.parseEther("1"); const amountOutMinimum = 2000000000; await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); await Helpers.expectToRevertWithCustomError( @@ -392,7 +398,6 @@ contract("SwapAdapter", async (accounts) => { it("should fail if msg.value is less than fee", async () => { const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; - const amount = Ethers.utils.parseEther("1"); const amountOutMinimum = 2000000000; await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); await Helpers.expectToRevertWithCustomError( From 0813be7909600e67b327ea04eada42b707bb4b39 Mon Sep 17 00:00:00 2001 From: viatrix Date: Mon, 28 Oct 2024 17:21:25 +0200 Subject: [PATCH 16/28] Add functions for swap with/without message --- contracts/adapters/SwapAdapter.sol | 202 ++++++++++++++---- .../interfaces/INativeTokenAdapter.sol | 4 + testUnderForked/swapAdapter.js | 122 +++++++++-- 3 files changed, 273 insertions(+), 55 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index a5aacbf8..9e91272a 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -79,6 +79,122 @@ contract SwapAdapter is AccessControl { emit TokenResourceIDSet(token, resourceID); } + /** + @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. + @param destinationDomainID ID of chain deposit will be bridged to. + @param recipient Recipient of the deposit. + @param token Input token to be swapped. + @param tokenAmount Amount of tokens to be swapped. + @param amountOutMinimum Minimal amount of ETH to be accepted as a swap output. + @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. + @param pathFees Fees for Uniswap pools. + */ + function depositTokensToEth( + uint8 destinationDomainID, + address recipient, + address token, + uint256 tokenAmount, + uint256 amountOutMinimum, + address[] calldata pathTokens, + uint24[] calldata pathFees + ) external { + if (tokenToResourceID[token] == bytes32(0)) revert TokenInvalid(); + + // Swap all tokens to ETH (exact input) + IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); + IERC20(token).safeApprove(address(_swapRouter), tokenAmount); + + uint256 amount = swapTokens( + pathTokens, + pathFees, + token, + _weth, + tokenAmount, + amountOutMinimum, + 0 + ); + + IWETH(_weth).withdraw(amount); + + // Make Native Token deposit + _nativeTokenAdapter.depositToEVM{value: amount}(destinationDomainID, recipient); + + // Return unspent fee to msg.sender + uint256 leftover = address(this).balance; + if (leftover > 0) { + payable(msg.sender).call{value: leftover}(""); + // Do not revert if sender does not want to receive. + } + } + + /** + @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. + @param destinationDomainID ID of chain deposit will be bridged to. + @param recipient Recipient of the deposit. + @param token Output token to be deposited after swapping. + @param amountOutMinimum Minimal amount of tokens to be accepted as a swap output. + @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. + @param pathFees Fees for Uniswap pools. + */ + function depositEthToTokens( + uint8 destinationDomainID, + address recipient, + address token, + uint256 amountOutMinimum, + address[] calldata pathTokens, + uint24[] calldata pathFees + ) external payable { + LocalVars memory vars; + vars.resourceID = tokenToResourceID[token]; + if (vars.resourceID == bytes32(0)) revert TokenInvalid(); + + // Compose depositData + vars.depositDataAfterAmount = abi.encodePacked( + uint256(20), + recipient + ); + if (msg.value == 0) revert InsufficientAmount(msg.value); + + vars.feeHandlerRouter = _bridge._feeHandler(); + (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + vars.resourceID, + abi.encodePacked(msg.value, vars.depositDataAfterAmount), + "" // feeData - not parsed + ); + + if (msg.value < vars.fee) revert MsgValueLowerThanFee(msg.value); + // Convert everything except the fee + vars.swapAmount = msg.value - vars.fee; + vars.amountOut = swapTokens( + pathTokens, + pathFees, + _weth, + token, + vars.swapAmount, + amountOutMinimum, + vars.swapAmount + ); + + vars.depositData = abi.encodePacked( + vars.amountOut, + vars.depositDataAfterAmount + ); + + vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); + IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), vars.amountOut); + _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); + + // Return unspent fee to msg.sender + vars.leftover = address(this).balance; + if (vars.leftover > 0) { + payable(msg.sender).call{value: vars.leftover}(""); + // Do not revert if sender does not want to receive. + } + } + /** @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. @param destinationDomainID ID of chain deposit will be bridged to. @@ -95,7 +211,7 @@ contract SwapAdapter is AccessControl { @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. @param pathFees Fees for Uniswap pools. */ - function depositTokensToEth( + function depositTokensToEthWithMessage( uint8 destinationDomainID, address recipient, uint256 gas, @@ -103,8 +219,8 @@ contract SwapAdapter is AccessControl { address token, uint256 tokenAmount, uint256 amountOutMinimum, - address[] memory pathTokens, - uint24[] memory pathFees + address[] calldata pathTokens, + uint24[] calldata pathFees ) external { if (tokenToResourceID[token] == bytes32(0)) revert TokenInvalid(); @@ -112,26 +228,16 @@ contract SwapAdapter is AccessControl { IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); IERC20(token).safeApprove(address(_swapRouter), tokenAmount); - uint256 amount; - - { - bytes memory path = _verifyAndEncodePath( - pathTokens, - pathFees, - token, - _weth - ); - IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter.ExactInputParams({ - path: path, - recipient: address(this), - amountIn: tokenAmount, - amountOutMinimum: amountOutMinimum - }); - - amount = _swapRouter.exactInput(params); - } + uint256 amount = swapTokens( + pathTokens, + pathFees, + token, + _weth, + tokenAmount, + amountOutMinimum, + 0 + ); - emit TokensSwapped(_weth, amount); IWETH(_weth).withdraw(amount); // Make Native Token deposit @@ -165,15 +271,15 @@ contract SwapAdapter is AccessControl { @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. @param pathFees Fees for Uniswap pools. */ - function depositEthToTokens( + function depositEthToTokensWithMessage( uint8 destinationDomainID, address recipient, uint256 gas, bytes calldata message, address token, uint256 amountOutMinimum, - address[] memory pathTokens, - uint24[] memory pathFees + address[] calldata pathTokens, + uint24[] calldata pathFees ) external payable { LocalVars memory vars; vars.resourceID = tokenToResourceID[token]; @@ -202,21 +308,15 @@ contract SwapAdapter is AccessControl { vars.swapAmount = msg.value - vars.fee; // Convert everything except the fee - // Swap ETH to tokens (exact input) - vars.path = _verifyAndEncodePath( + vars.amountOut = swapTokens( pathTokens, pathFees, _weth, - token + token, + vars.swapAmount, + amountOutMinimum, + vars.swapAmount ); - vars.params = IV3SwapRouter.ExactInputParams({ - path: vars.path, - recipient: address(this), - amountIn: vars.swapAmount, - amountOutMinimum: amountOutMinimum - }); - - vars.amountOut = _swapRouter.exactInput{value: vars.swapAmount}(vars.params); emit TokensSwapped(token, vars.amountOut); vars.depositData = abi.encodePacked( @@ -236,9 +336,35 @@ contract SwapAdapter is AccessControl { } } + function swapTokens( + address[] calldata pathTokens, + uint24[] calldata pathFees, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMinimum, + uint256 valueToSend + ) internal returns(uint256 amount) { + bytes memory path = _verifyAndEncodePath( + pathTokens, + pathFees, + tokenIn, + tokenOut + ); + IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + amountIn: amountIn, + amountOutMinimum: amountOutMinimum + }); + + amount = _swapRouter.exactInput{value: valueToSend}(params); + emit TokensSwapped(tokenOut, amount); + } + function _verifyAndEncodePath( - address[] memory tokens, - uint24[] memory fees, + address[] calldata tokens, + uint24[] calldata fees, address tokenIn, address tokenOut ) internal view returns (bytes memory path) { diff --git a/contracts/adapters/interfaces/INativeTokenAdapter.sol b/contracts/adapters/interfaces/INativeTokenAdapter.sol index baaf2672..48fc769a 100644 --- a/contracts/adapters/interfaces/INativeTokenAdapter.sol +++ b/contracts/adapters/interfaces/INativeTokenAdapter.sol @@ -7,6 +7,10 @@ pragma solidity 0.8.11; @author ChainSafe Systems. */ interface INativeTokenAdapter { + function depositToEVM( + uint8 destinationDomainID, + address recipientAddress + ) external payable; function depositToEVMWithMessage( uint8 destinationDomainID, diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 1f07af10..928b7596 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -135,7 +135,6 @@ contract("SwapAdapter", async (accounts) => { // set MPC address to unpause the Bridge await BridgeInstance.endKeygen(Helpers.mpcAddress); - }); it("should swap tokens to ETH and bridge ETH", async () => { @@ -146,6 +145,109 @@ contract("SwapAdapter", async (accounts) => { await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); const depositTx = await SwapAdapterInstance.depositTokensToEth( + destinationDomainID, + recipientAddress, + USDC_ADDRESS, + amount, + amountOutMinimum, + pathTokens, + pathFees, + {from: USDC_OWNER_ADDRESS} + ); + expect(await web3.eth.getBalance(SwapAdapterInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(BasicFeeHandlerInstance.address)).to.eq(fee.toString()); + expect(await web3.eth.getBalance(NativeTokenHandlerInstance.address)).to.not.eq("0"); + + const depositCount = await BridgeInstance._depositCounts.call( + destinationDomainID + ); + const expectedDepositNonce = 1; + assert.strictEqual(depositCount.toNumber(), expectedDepositNonce); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const amountOut = events[events.length - 1].args.amountOut; + + const depositData = await Helpers.createERCDepositData(amountOut - fee, 20, recipientAddress); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID_Native.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === NativeTokenAdapterInstance.address && + event.data === depositData.toLowerCase() && + event.handlerResponse === null + ); + }); + }); + + it("should swap ETH to tokens and bridge tokens", async () => { + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathFees = [500]; + const amount = Ethers.utils.parseEther("1"); + const amountOutMinimum = 2000000000; + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + const depositTx = await SwapAdapterInstance.depositEthToTokens( + destinationDomainID, + recipientAddress, + USDC_ADDRESS, + amountOutMinimum, + pathTokens, + pathFees, + { + value: amount, + from: depositorAddress + } + ); + expect((await usdc.balanceOf(SwapAdapterInstance.address)).toString()).to.eq("0"); + expect(await web3.eth.getBalance(SwapAdapterInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(BridgeInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(FeeHandlerRouterInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(BasicFeeHandlerInstance.address)).to.eq(fee.toString()); + expect(await usdc.balanceOf(ERC20HandlerInstance.address)).to.not.eq("0"); + + const depositCount = await BridgeInstance._depositCounts.call( + destinationDomainID + ); + const expectedDepositNonce = 1; + assert.strictEqual(depositCount.toNumber(), expectedDepositNonce); + + const internalTx = await TruffleAssert.createTransactionResult( + BridgeInstance, + depositTx.tx + ); + + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const amountOut = events[events.length - 1].args.amountOut; + expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); + + const depositData = await Helpers.createERCDepositData(amountOut.toNumber(), 20, recipientAddress); + + TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { + return ( + event.destinationDomainID.toNumber() === destinationDomainID && + event.resourceID === resourceID_USDC.toLowerCase() && + event.depositNonce.toNumber() === expectedDepositNonce && + event.user === SwapAdapterInstance.address && + event.data === depositData.toLowerCase() && + event.handlerResponse === null + ); + }); + }); + + it("should swap tokens to ETH and bridge ETH with contract call", async () => { + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathFees = [500]; + const amount = 1000000; + const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + const depositTx = await SwapAdapterInstance.depositTokensToEthWithMessage( destinationDomainID, recipientAddress, executionGasAmount, @@ -193,13 +295,13 @@ contract("SwapAdapter", async (accounts) => { }); }); - it("should swap ETH to tokens and bridge tokens", async () => { + it("should swap ETH to tokens and bridge tokens with contract call", async () => { const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; const amount = Ethers.utils.parseEther("1"); const amountOutMinimum = 2000000000; await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); - const depositTx = await SwapAdapterInstance.depositEthToTokens( + const depositTx = await SwapAdapterInstance.depositEthToTokensWithMessage( destinationDomainID, recipientAddress, executionGasAmount, @@ -264,8 +366,6 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth( destinationDomainID, recipientAddress, - executionGasAmount, - message, USDC_ADDRESS, amount, amountOutMinimum, @@ -287,8 +387,6 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, - executionGasAmount, - message, USDC_ADDRESS, amount, amountOutMinimum, @@ -311,8 +409,6 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, - executionGasAmount, - message, USDC_ADDRESS, amount, amountOutMinimum, @@ -335,8 +431,6 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, - executionGasAmount, - message, USDC_ADDRESS, amount, amountOutMinimum, @@ -358,8 +452,6 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositTokensToEth.call( destinationDomainID, recipientAddress, - executionGasAmount, - message, USDC_ADDRESS, amount, amountOutMinimum, @@ -380,8 +472,6 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositEthToTokens.call( destinationDomainID, recipientAddress, - executionGasAmount, - message, USDC_ADDRESS, amountOutMinimum, pathTokens, @@ -404,8 +494,6 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance.depositEthToTokens.call( destinationDomainID, recipientAddress, - executionGasAmount, - message, USDC_ADDRESS, amountOutMinimum, pathTokens, From f281696fbf995404678c68d98b24db9e3fe7e5ac Mon Sep 17 00:00:00 2001 From: viatrix Date: Mon, 28 Oct 2024 17:28:51 +0200 Subject: [PATCH 17/28] Fix lint --- testUnderForked/swapAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 928b7596..15d8f346 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -169,7 +169,7 @@ contract("SwapAdapter", async (accounts) => { depositTx.tx ); - const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); const amountOut = events[events.length - 1].args.amountOut; const depositData = await Helpers.createERCDepositData(amountOut - fee, 20, recipientAddress); @@ -222,7 +222,7 @@ contract("SwapAdapter", async (accounts) => { depositTx.tx ); - const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", { fromBlock: depositTx.receipt.blockNumber }); + const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); const amountOut = events[events.length - 1].args.amountOut; expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); From 91f8ae29926a190a9c6262359ee591ba9e22a36b Mon Sep 17 00:00:00 2001 From: viatrix Date: Thu, 31 Oct 2024 10:17:05 +0200 Subject: [PATCH 18/28] Update comments --- contracts/adapters/SwapAdapter.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 9e91272a..07c30028 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -128,7 +128,8 @@ contract SwapAdapter is AccessControl { } /** - @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. + @notice Function for depositing ETH, performing swap to defined tokens and bridging + the tokens. @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. @param token Output token to be deposited after swapping. @@ -196,7 +197,8 @@ contract SwapAdapter is AccessControl { } /** - @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. + @notice Function for depositing tokens, performing swap to ETH, bridging the ETH and executing + a contract call on destination. @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. @param gas The amount of gas needed to successfully execute the call to recipient on the destination. Fee amount is @@ -257,7 +259,8 @@ contract SwapAdapter is AccessControl { } /** - @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. + @notice Function for depositing ETH, performing swap to defined tokens, bridging + the tokens and executing a contract call on destination. @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. @param gas The amount of gas needed to successfully execute the call to recipient on the destination. Fee amount is From 4510a86dfd23fe1a6b24f40244978d8fb489879c Mon Sep 17 00:00:00 2001 From: viatrix Date: Mon, 4 Nov 2024 21:38:42 +0200 Subject: [PATCH 19/28] Implement exactOutput swaps --- contracts/adapters/SwapAdapter.sol | 222 ++++++++++++------ .../interfaces/IPeripheryPayments.sol | 28 +++ testUnderForked/swapAdapter.js | 108 ++++++--- 3 files changed, 244 insertions(+), 114 deletions(-) create mode 100644 contracts/adapters/interfaces/IPeripheryPayments.sol diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 07c30028..1c5a7af5 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -10,6 +10,7 @@ import "../../contracts/interfaces/IFeeHandler.sol"; import "../../contracts/adapters/interfaces/INativeTokenAdapter.sol"; import "../../contracts/adapters/interfaces/IWETH.sol"; import "../../contracts/adapters/interfaces/IV3SwapRouter.sol"; +import "../../contracts/adapters/interfaces/IPeripheryPayments.sol"; /** @title Contract that swaps tokens to ETH or ETH to tokens using Uniswap @@ -29,17 +30,18 @@ contract SwapAdapter is AccessControl { // Used to avoid "stack too deep" error struct LocalVars { - bytes32 resourceID; - bytes depositDataAfterAmount; uint256 fee; - address feeHandlerRouter; - uint256 amountOut; + uint256 totalAmountOut; + uint256 amountIn; uint256 swapAmount; + address feeHandlerRouter; + bytes32 resourceID; + address ERC20HandlerAddress; + uint256 leftover; + bytes depositDataAfterAmount; bytes path; IV3SwapRouter.ExactInputParams params; bytes depositData; - address ERC20HandlerAddress; - uint256 leftover; } error CallerNotAdmin(); @@ -52,7 +54,7 @@ contract SwapAdapter is AccessControl { error AmountLowerThanFee(uint256 amount); event TokenResourceIDSet(address token, bytes32 resourceID); - event TokensSwapped(address token, uint256 amountOut); + event TokensSwapped(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut); constructor( IBridge bridge, @@ -83,41 +85,69 @@ contract SwapAdapter is AccessControl { @notice Function for depositing tokens, performing swap to ETH and bridging the ETH. @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. - @param token Input token to be swapped. - @param tokenAmount Amount of tokens to be swapped. - @param amountOutMinimum Minimal amount of ETH to be accepted as a swap output. - @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. - @param pathFees Fees for Uniswap pools. + @param token Input token to be swapped. + @param amountInMax Max amount of input tokens to be swapped. It should exceed the desired + amount of output tokens because the amount of swapped ETH should also cover the bridging fee. + It's equal to tokenSwapRate * (amountOut + bridging fee) + @param amountOut Amount of ETH to be bridged. + @param pathTokens Addresses of the tokens for Uniswap swap (in reverse order). WETH address is used for ETH. + @param pathFees Fees for Uniswap pools (in reverse order). */ function depositTokensToEth( uint8 destinationDomainID, address recipient, address token, - uint256 tokenAmount, - uint256 amountOutMinimum, + uint256 amountInMax, + uint256 amountOut, address[] calldata pathTokens, uint24[] calldata pathFees ) external { - if (tokenToResourceID[token] == bytes32(0)) revert TokenInvalid(); + LocalVars memory vars; + vars.resourceID = tokenToResourceID[token]; + if (vars.resourceID == bytes32(0)) revert TokenInvalid(); - // Swap all tokens to ETH (exact input) - IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); - IERC20(token).safeApprove(address(_swapRouter), tokenAmount); + // Compose depositData + vars.depositDataAfterAmount = abi.encodePacked( + uint256(20), + recipient + ); - uint256 amount = swapTokens( + vars.feeHandlerRouter = _bridge._feeHandler(); + (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + vars.resourceID, + abi.encodePacked(amountOut, vars.depositDataAfterAmount), + "" // feeData - not parsed + ); + + vars.totalAmountOut = amountOut + vars.fee; + + // Swap tokens to ETH (exact output) + IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); + IERC20(token).safeApprove(address(_swapRouter), amountInMax); + + vars.amountIn = swapTokens( pathTokens, pathFees, token, _weth, - tokenAmount, - amountOutMinimum, + amountInMax, + vars.totalAmountOut, 0 ); - IWETH(_weth).withdraw(amount); + IWETH(_weth).withdraw(vars.totalAmountOut); // Make Native Token deposit - _nativeTokenAdapter.depositToEVM{value: amount}(destinationDomainID, recipient); + _nativeTokenAdapter.depositToEVM{value: vars.totalAmountOut}(destinationDomainID, recipient); + + // Refund tokens + if (vars.amountIn < amountInMax) { + IERC20(token).safeApprove(address(_swapRouter), 0); + IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + } // Return unspent fee to msg.sender uint256 leftover = address(this).balance; @@ -130,18 +160,21 @@ contract SwapAdapter is AccessControl { /** @notice Function for depositing ETH, performing swap to defined tokens and bridging the tokens. + msg.value should not only cover the swap for desired amount of tokens + but it should also cover the bridging fee. + It's equal to bridging fee + tokenSwapRate * amountOut @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. - @param token Output token to be deposited after swapping. - @param amountOutMinimum Minimal amount of tokens to be accepted as a swap output. - @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. - @param pathFees Fees for Uniswap pools. + @param token Output token to be deposited after swapping. + @param amountOut Amount of tokens to be bridged. + @param pathTokens Addresses of the tokens for Uniswap swap (in reverse order). WETH address is used for ETH. + @param pathFees Fees for Uniswap pools (in reverse order). */ function depositEthToTokens( uint8 destinationDomainID, address recipient, address token, - uint256 amountOutMinimum, + uint256 amountOut, address[] calldata pathTokens, uint24[] calldata pathFees ) external payable { @@ -169,26 +202,27 @@ contract SwapAdapter is AccessControl { if (msg.value < vars.fee) revert MsgValueLowerThanFee(msg.value); // Convert everything except the fee vars.swapAmount = msg.value - vars.fee; - vars.amountOut = swapTokens( + vars.amountIn = swapTokens( pathTokens, pathFees, _weth, token, vars.swapAmount, - amountOutMinimum, + amountOut, vars.swapAmount ); + IPeripheryPayments(address(_swapRouter)).refundETH(); vars.depositData = abi.encodePacked( - vars.amountOut, + amountOut, vars.depositDataAfterAmount ); vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); - IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), vars.amountOut); + IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); - // Return unspent fee to msg.sender + // Return unspent native currency to msg.sender vars.leftover = address(this).balance; if (vars.leftover > 0) { payable(msg.sender).call{value: vars.leftover}(""); @@ -208,59 +242,93 @@ contract SwapAdapter is AccessControl { DefaultMessageReceiver, make sure to encode the message to comply with the DefaultMessageReceiver.handleSygmaMessage() message decoding implementation. @param token Input token to be swapped. - @param tokenAmount Amount of tokens to be swapped. - @param amountOutMinimum Minimal amount of ETH to be accepted as a swap output. - @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. - @param pathFees Fees for Uniswap pools. + @param amountInMax Max amount of input tokens to be swapped. It should exceed the desired + amount of output tokens because the amount of swapped ETH should also cover the bridging fee. + It's equal to tokenSwapRate * (amountOut + bridging fee) + @param amountOut Amount of ETH to be bridged. + @param pathTokens Addresses of the tokens for Uniswap swap (in reverse order). WETH address is used for ETH. + @param pathFees Fees for Uniswap pools (in reverse order). */ function depositTokensToEthWithMessage( uint8 destinationDomainID, address recipient, uint256 gas, - bytes calldata message, + bytes memory message, address token, - uint256 tokenAmount, - uint256 amountOutMinimum, + uint256 amountInMax, + uint256 amountOut, address[] calldata pathTokens, uint24[] calldata pathFees ) external { - if (tokenToResourceID[token] == bytes32(0)) revert TokenInvalid(); + LocalVars memory vars; + vars.resourceID = tokenToResourceID[token]; + if (vars.resourceID == bytes32(0)) revert TokenInvalid(); + + // Compose depositData + vars.depositDataAfterAmount = abi.encodePacked( + uint256(20), + recipient, + gas, + uint256(message.length), + message + ); + + vars.feeHandlerRouter = _bridge._feeHandler(); + (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( + address(this), + _bridge._domainID(), + destinationDomainID, + vars.resourceID, + abi.encodePacked(amountOut, vars.depositDataAfterAmount), + "" // feeData - not parsed + ); + + vars.totalAmountOut = amountOut + vars.fee; - // Swap all tokens to ETH (exact input) - IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); - IERC20(token).safeApprove(address(_swapRouter), tokenAmount); + // Swap tokens to ETH (exact output) + IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); + IERC20(token).safeApprove(address(_swapRouter), amountInMax); - uint256 amount = swapTokens( + vars.amountIn = swapTokens( pathTokens, pathFees, token, _weth, - tokenAmount, - amountOutMinimum, + amountInMax, + vars.totalAmountOut, 0 ); - IWETH(_weth).withdraw(amount); + IWETH(_weth).withdraw(vars.totalAmountOut); + + // Refund tokens + if (vars.amountIn < amountInMax) { + IERC20(token).safeApprove(address(_swapRouter), 0); + IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + } // Make Native Token deposit - _nativeTokenAdapter.depositToEVMWithMessage{value: amount}( + _nativeTokenAdapter.depositToEVMWithMessage{value: vars.totalAmountOut}( destinationDomainID, recipient, gas, message ); - // Return unspent fee to msg.sender - uint256 leftover = address(this).balance; - if (leftover > 0) { - payable(msg.sender).call{value: leftover}(""); + // Return unspent native currency to msg.sender + vars.leftover = address(this).balance; + if (vars.leftover > 0) { + payable(msg.sender).call{value: vars.leftover}(""); // Do not revert if sender does not want to receive. } } /** - @notice Function for depositing ETH, performing swap to defined tokens, bridging - the tokens and executing a contract call on destination. + @notice Function for depositing ETH, performing swap to defined tokens and bridging + the tokens. + msg.value should not only cover the swap for desired amount of tokens + but it should also cover the bridging fee. + It's equal to bridging fee + tokenSwapRate * amountOut @param destinationDomainID ID of chain deposit will be bridged to. @param recipient Recipient of the deposit. @param gas The amount of gas needed to successfully execute the call to recipient on the destination. Fee amount is @@ -270,9 +338,9 @@ contract SwapAdapter is AccessControl { DefaultMessageReceiver, make sure to encode the message to comply with the DefaultMessageReceiver.handleSygmaMessage() message decoding implementation. @param token Output token to be deposited after swapping. - @param amountOutMinimum Minimal amount of tokens to be accepted as a swap output. - @param pathTokens Addresses of the tokens for Uniswap swap. WETH address is used for ETH. - @param pathFees Fees for Uniswap pools. + @param amountOut Amount of tokens to be bridged. + @param pathTokens Addresses of the tokens for Uniswap swap (in reverse order). WETH address is used for ETH. + @param pathFees Fees for Uniswap pools (in reverse order). */ function depositEthToTokensWithMessage( uint8 destinationDomainID, @@ -280,7 +348,7 @@ contract SwapAdapter is AccessControl { uint256 gas, bytes calldata message, address token, - uint256 amountOutMinimum, + uint256 amountOut, address[] calldata pathTokens, uint24[] calldata pathFees ) external payable { @@ -297,6 +365,7 @@ contract SwapAdapter is AccessControl { message ); if (msg.value == 0) revert InsufficientAmount(msg.value); + vars.feeHandlerRouter = _bridge._feeHandler(); (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( address(this), @@ -308,30 +377,29 @@ contract SwapAdapter is AccessControl { ); if (msg.value < vars.fee) revert MsgValueLowerThanFee(msg.value); - vars.swapAmount = msg.value - vars.fee; // Convert everything except the fee - - vars.amountOut = swapTokens( + vars.swapAmount = msg.value - vars.fee; + vars.amountIn = swapTokens( pathTokens, pathFees, _weth, token, vars.swapAmount, - amountOutMinimum, + amountOut, vars.swapAmount ); - emit TokensSwapped(token, vars.amountOut); + IPeripheryPayments(address(_swapRouter)).refundETH(); vars.depositData = abi.encodePacked( - vars.amountOut, + amountOut, vars.depositDataAfterAmount ); vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); - IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), vars.amountOut); + IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); - // Return unspent fee to msg.sender + // Return unspent native currency to msg.sender vars.leftover = address(this).balance; if (vars.leftover > 0) { payable(msg.sender).call{value: vars.leftover}(""); @@ -344,25 +412,25 @@ contract SwapAdapter is AccessControl { uint24[] calldata pathFees, address tokenIn, address tokenOut, - uint256 amountIn, - uint256 amountOutMinimum, + uint256 amountInMaximum, + uint256 amountOut, uint256 valueToSend - ) internal returns(uint256 amount) { + ) internal returns(uint256 amountIn) { bytes memory path = _verifyAndEncodePath( pathTokens, pathFees, tokenIn, tokenOut ); - IV3SwapRouter.ExactInputParams memory params = IV3SwapRouter.ExactInputParams({ + IV3SwapRouter.ExactOutputParams memory params = IV3SwapRouter.ExactOutputParams({ path: path, recipient: address(this), - amountIn: amountIn, - amountOutMinimum: amountOutMinimum + amountOut: amountOut, + amountInMaximum: amountInMaximum }); - amount = _swapRouter.exactInput{value: valueToSend}(params); - emit TokensSwapped(tokenOut, amount); + amountIn = _swapRouter.exactOutput{value: valueToSend}(params); + emit TokensSwapped(tokenIn, tokenOut, amountIn, amountOut); } function _verifyAndEncodePath( @@ -376,10 +444,10 @@ contract SwapAdapter is AccessControl { } tokenIn = tokenIn == address(0) ? address(_weth) : tokenIn; - if (tokens[0] != tokenIn) revert PathInvalid(); + if (tokens[tokens.length - 1] != tokenIn) revert PathInvalid(); tokenOut = tokenOut == address(0) ? address(_weth) : tokenOut; - if (tokens[tokens.length - 1] != tokenOut) revert PathInvalid(); + if (tokens[0] != tokenOut) revert PathInvalid(); for (uint256 i = 0; i < tokens.length - 1; i++){ path = abi.encodePacked(path, tokens[i], fees[i]); diff --git a/contracts/adapters/interfaces/IPeripheryPayments.sol b/contracts/adapters/interfaces/IPeripheryPayments.sol new file mode 100644 index 00000000..ac74f8c9 --- /dev/null +++ b/contracts/adapters/interfaces/IPeripheryPayments.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; + +/// @title Periphery Payments +/// @notice Functions to ease deposits and withdrawals of ETH +interface IPeripheryPayments { + /// @notice Unwraps the contract's WETH9 balance and sends it to recipient as ETH. + /// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users. + /// @param amountMinimum The minimum amount of WETH9 to unwrap + /// @param recipient The address receiving ETH + function unwrapWETH9(uint256 amountMinimum, address recipient) external payable; + + /// @notice Refunds any ETH balance held by this contract to the `msg.sender` + /// @dev Useful for bundling with mint or increase liquidity that uses ether, or exact output swaps + /// that use ether for the input amount + function refundETH() external payable; + + /// @notice Transfers the full amount of a token held by this contract to recipient + /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users + /// @param token The contract address of the token which will be transferred to `recipient` + /// @param amountMinimum The minimum amount of token required for a transfer + /// @param recipient The destination address of the token + function sweepToken( + address token, + uint256 amountMinimum, + address recipient + ) external payable; +} diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 15d8f346..df7863bb 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -23,7 +23,7 @@ contract("SwapAdapter", async (accounts) => { // use SwapRouter, USDC, WETH, user with USDC, user with ETH from mainnet fork const recipientAddress = accounts[2]; const fee = 1000; - const depositorAddress = accounts[1]; + const depositorAddress = accounts[3]; const emptySetResourceData = "0x"; const originDomainID = 1; const destinationDomainID = 3; @@ -49,6 +49,7 @@ contract("SwapAdapter", async (accounts) => { let NativeTokenHandlerInstance; let SwapAdapterInstance; let usdc; + let weth; let message; beforeEach(async () => { @@ -88,6 +89,7 @@ contract("SwapAdapter", async (accounts) => { NativeTokenAdapterInstance.address ); usdc = await ERC20MintableContract.at(USDC_ADDRESS); + weth = await ERC20MintableContract.at(WETH_ADDRESS); await BridgeInstance.adminSetResource( NativeTokenHandlerInstance.address, @@ -138,18 +140,19 @@ contract("SwapAdapter", async (accounts) => { }); it("should swap tokens to ETH and bridge ETH", async () => { - const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; - const amount = 1000000; - const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + const amountInMax = 1000000; + const amountOut = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); - await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + await usdc.approve(SwapAdapterInstance.address, amountInMax, {from: USDC_OWNER_ADDRESS}); + const balanceBefore = await usdc.balanceOf(USDC_OWNER_ADDRESS); const depositTx = await SwapAdapterInstance.depositTokensToEth( destinationDomainID, recipientAddress, USDC_ADDRESS, - amount, - amountOutMinimum, + amountInMax, + amountOut, pathTokens, pathFees, {from: USDC_OWNER_ADDRESS} @@ -170,9 +173,13 @@ contract("SwapAdapter", async (accounts) => { ); const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); - const amountOut = events[events.length - 1].args.amountOut; + const amountIn = events[events.length - 1].args.amountIn; - const depositData = await Helpers.createERCDepositData(amountOut - fee, 20, recipientAddress); + const balanceAfter = await usdc.balanceOf(USDC_OWNER_ADDRESS); + expect(balanceAfter.toNumber()).to.eq(balanceBefore - amountIn); + expect(balanceAfter.toNumber()).to.be.gt(balanceBefore - amountInMax); + + const depositData = await Helpers.createERCDepositData(amountOut, 20, recipientAddress); TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { return ( @@ -187,29 +194,37 @@ contract("SwapAdapter", async (accounts) => { }); it("should swap ETH to tokens and bridge tokens", async () => { - const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; const pathFees = [500]; - const amount = Ethers.utils.parseEther("1"); - const amountOutMinimum = 2000000000; + const amountInMax = Ethers.utils.parseEther("1"); + const amountOut = 2000000000; await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + const balanceBefore = await web3.eth.getBalance(depositorAddress); const depositTx = await SwapAdapterInstance.depositEthToTokens( destinationDomainID, recipientAddress, USDC_ADDRESS, - amountOutMinimum, + amountOut, pathTokens, pathFees, { - value: amount, + value: amountInMax, from: depositorAddress } ); + const balanceAfter = await web3.eth.getBalance(depositorAddress); expect((await usdc.balanceOf(SwapAdapterInstance.address)).toString()).to.eq("0"); expect(await web3.eth.getBalance(SwapAdapterInstance.address)).to.eq("0"); expect(await web3.eth.getBalance(BridgeInstance.address)).to.eq("0"); expect(await web3.eth.getBalance(FeeHandlerRouterInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(ERC20HandlerInstance.address)).to.eq("0"); + expect(await web3.eth.getBalance(UNISWAP_SWAP_ROUTER_ADDRESS)).to.eq("0"); + expect((await weth.balanceOf(UNISWAP_SWAP_ROUTER_ADDRESS)).toNumber()).to.eq(0); + expect((await weth.balanceOf(SwapAdapterInstance.address)).toNumber()).to.eq(0); + expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); + expect(await web3.eth.getBalance(BasicFeeHandlerInstance.address)).to.eq(fee.toString()); - expect(await usdc.balanceOf(ERC20HandlerInstance.address)).to.not.eq("0"); + expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toNumber()).to.eq(amountOut); const depositCount = await BridgeInstance._depositCounts.call( destinationDomainID @@ -223,10 +238,15 @@ contract("SwapAdapter", async (accounts) => { ); const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); - const amountOut = events[events.length - 1].args.amountOut; - expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); + const amountIn = events[events.length - 1].args.amountIn; + const gasPrice = (await web3.eth.getTransactionReceipt( + depositTx.tx + )).effectiveGasPrice; + const txCost = gasPrice * depositTx.receipt.gasUsed; + assert.equal(Ethers.BigNumber.from(balanceAfter).toString(), + Ethers.BigNumber.from(balanceBefore).sub(amountIn.toString()).sub(txCost).sub(fee).toString()); - const depositData = await Helpers.createERCDepositData(amountOut.toNumber(), 20, recipientAddress); + const depositData = await Helpers.createERCDepositData(amountOut, 20, recipientAddress); TruffleAssert.eventEmitted(internalTx, "Deposit", (event) => { return ( @@ -241,20 +261,21 @@ contract("SwapAdapter", async (accounts) => { }); it("should swap tokens to ETH and bridge ETH with contract call", async () => { - const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; - const amount = 1000000; - const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + const amountInMax = 1000000; + const amountOut = Ethers.utils.parseUnits("200000", "gwei"); await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); - await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); + await usdc.approve(SwapAdapterInstance.address, amountInMax, {from: USDC_OWNER_ADDRESS}); + const balanceBefore = await usdc.balanceOf(USDC_OWNER_ADDRESS); const depositTx = await SwapAdapterInstance.depositTokensToEthWithMessage( destinationDomainID, recipientAddress, executionGasAmount, message, USDC_ADDRESS, - amount, - amountOutMinimum, + amountInMax, + amountOut, pathTokens, pathFees, {from: USDC_OWNER_ADDRESS} @@ -274,10 +295,14 @@ contract("SwapAdapter", async (accounts) => { ); const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); - const amountOut = events[events.length - 1].args.amountOut; + const amountIn = events[events.length - 1].args.amountIn; + + const balanceAfter = await usdc.balanceOf(USDC_OWNER_ADDRESS); + expect(balanceAfter.toNumber()).to.eq(balanceBefore - amountIn); + expect(balanceAfter.toNumber()).to.be.gt(balanceBefore - amountInMax); const depositData = await Helpers.createOptionalContractCallDepositData( - amountOut - fee, + amountOut, recipientAddress, executionGasAmount, message @@ -296,31 +321,34 @@ contract("SwapAdapter", async (accounts) => { }); it("should swap ETH to tokens and bridge tokens with contract call", async () => { - const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; const pathFees = [500]; - const amount = Ethers.utils.parseEther("1"); - const amountOutMinimum = 2000000000; + const amountInMax = Ethers.utils.parseEther("1"); + const amountOut = 2000000000; await SwapAdapterInstance.setTokenResourceID(USDC_ADDRESS, resourceID_USDC); + const balanceBefore = await web3.eth.getBalance(depositorAddress); const depositTx = await SwapAdapterInstance.depositEthToTokensWithMessage( destinationDomainID, recipientAddress, executionGasAmount, message, USDC_ADDRESS, - amountOutMinimum, + amountOut, pathTokens, pathFees, { - value: amount, + value: amountInMax, from: depositorAddress } ); + const balanceAfter = await web3.eth.getBalance(depositorAddress); expect((await usdc.balanceOf(SwapAdapterInstance.address)).toString()).to.eq("0"); expect(await web3.eth.getBalance(SwapAdapterInstance.address)).to.eq("0"); expect(await web3.eth.getBalance(BridgeInstance.address)).to.eq("0"); expect(await web3.eth.getBalance(FeeHandlerRouterInstance.address)).to.eq("0"); expect(await web3.eth.getBalance(BasicFeeHandlerInstance.address)).to.eq(fee.toString()); expect(await usdc.balanceOf(ERC20HandlerInstance.address)).to.not.eq("0"); + expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); const depositCount = await BridgeInstance._depositCounts.call( destinationDomainID @@ -334,11 +362,17 @@ contract("SwapAdapter", async (accounts) => { ); const events = await SwapAdapterInstance.getPastEvents("TokensSwapped", {fromBlock: depositTx.receipt.blockNumber}); - const amountOut = events[events.length - 1].args.amountOut; - expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); + const amountIn = events[events.length - 1].args.amountIn; + + const gasPrice = (await web3.eth.getTransactionReceipt( + depositTx.tx + )).effectiveGasPrice; + const txCost = gasPrice * depositTx.receipt.gasUsed; + assert.equal(Ethers.BigNumber.from(balanceAfter).toString(), + Ethers.BigNumber.from(balanceBefore).sub(amountIn.toString()).sub(txCost).sub(fee).toString()); const depositData = await Helpers.createOptionalContractCallDepositData( - amountOut.toNumber(), + amountOut, recipientAddress, executionGasAmount, message @@ -377,7 +411,7 @@ contract("SwapAdapter", async (accounts) => { }); it("should fail if the path is invalid [tokens length and fees length]", async () => { - const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500, 300]; const amount = 1000000; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); @@ -399,7 +433,7 @@ contract("SwapAdapter", async (accounts) => { }); it("should fail if the path is invalid [tokenIn is not token0]", async () => { - const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; const pathFees = [500]; const amount = 1000000; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); @@ -443,7 +477,7 @@ contract("SwapAdapter", async (accounts) => { }); it("should fail if the resource id is not configured", async () => { - const pathTokens = [USDC_ADDRESS, WETH_ADDRESS]; + const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; const pathFees = [500]; const amount = 1000000; const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); From 9b41ed6d39bf5b5e99ed6b29561ecaf350e3bb47 Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 5 Nov 2024 13:54:53 +0200 Subject: [PATCH 20/28] WIP: Make contract upgradeable --- contracts/adapters/SwapAdapter.sol | 79 +++++++++++++++++------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 1c5a7af5..16a0a862 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -2,9 +2,10 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity 0.8.11; -import "@openzeppelin/contracts/access/AccessControl.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "../../contracts/interfaces/IBridge.sol"; import "../../contracts/interfaces/IFeeHandler.sol"; import "../../contracts/adapters/interfaces/INativeTokenAdapter.sol"; @@ -17,14 +18,14 @@ import "../../contracts/adapters/interfaces/IPeripheryPayments.sol"; and then makes a deposit to the Bridge. @author ChainSafe Systems. */ -contract SwapAdapter is AccessControl { +contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { - using SafeERC20 for IERC20; + using SafeERC20Upgradeable for IERC20Upgradeable; IBridge public immutable _bridge; - address immutable _weth; - IV3SwapRouter public _swapRouter; - INativeTokenAdapter _nativeTokenAdapter; + address public immutable _weth; + IV3SwapRouter public immutable _swapRouter; + INativeTokenAdapter public immutable _nativeTokenAdapter; mapping(address => bytes32) public tokenToResourceID; @@ -56,26 +57,28 @@ contract SwapAdapter is AccessControl { event TokenResourceIDSet(address token, bytes32 resourceID); event TokensSwapped(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut); + /// @custom:oz-upgrades-unsafe-allow constructor constructor( IBridge bridge, address weth, IV3SwapRouter swapRouter, INativeTokenAdapter nativeTokenAdapter ) { + _disableInitializers(); _bridge = bridge; _weth = weth; _swapRouter = swapRouter; _nativeTokenAdapter = nativeTokenAdapter; - _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } - modifier onlyAdmin() { - if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert CallerNotAdmin(); - _; + + function initialize() public initializer { + _transferOwnership(msg.sender); + __UUPSUpgradeable_init(); } // Admin functions - function setTokenResourceID(address token, bytes32 resourceID) external onlyAdmin { + function setTokenResourceID(address token, bytes32 resourceID) external onlyOwner { if (tokenToResourceID[token] == resourceID) revert AlreadySet(); tokenToResourceID[token] = resourceID; emit TokenResourceIDSet(token, resourceID); @@ -125,8 +128,8 @@ contract SwapAdapter is AccessControl { vars.totalAmountOut = amountOut + vars.fee; // Swap tokens to ETH (exact output) - IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); - IERC20(token).safeApprove(address(_swapRouter), amountInMax); + IERC20Upgradeable(token).safeTransferFrom(msg.sender, address(this), amountInMax); + IERC20Upgradeable(token).safeApprove(address(_swapRouter), amountInMax); vars.amountIn = swapTokens( pathTokens, @@ -145,8 +148,8 @@ contract SwapAdapter is AccessControl { // Refund tokens if (vars.amountIn < amountInMax) { - IERC20(token).safeApprove(address(_swapRouter), 0); - IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + IERC20Upgradeable(token).safeApprove(address(_swapRouter), 0); + IERC20Upgradeable(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); } // Return unspent fee to msg.sender @@ -189,13 +192,18 @@ contract SwapAdapter is AccessControl { ); if (msg.value == 0) revert InsufficientAmount(msg.value); + vars.depositData = abi.encodePacked( + amountOut, + vars.depositDataAfterAmount + ); + vars.feeHandlerRouter = _bridge._feeHandler(); (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( address(this), _bridge._domainID(), destinationDomainID, vars.resourceID, - abi.encodePacked(msg.value, vars.depositDataAfterAmount), + vars.depositData, "" // feeData - not parsed ); @@ -213,13 +221,9 @@ contract SwapAdapter is AccessControl { ); IPeripheryPayments(address(_swapRouter)).refundETH(); - vars.depositData = abi.encodePacked( - amountOut, - vars.depositDataAfterAmount - ); vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); - IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); + IERC20Upgradeable(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); // Return unspent native currency to msg.sender @@ -286,8 +290,8 @@ contract SwapAdapter is AccessControl { vars.totalAmountOut = amountOut + vars.fee; // Swap tokens to ETH (exact output) - IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); - IERC20(token).safeApprove(address(_swapRouter), amountInMax); + IERC20Upgradeable(token).safeTransferFrom(msg.sender, address(this), amountInMax); + IERC20Upgradeable(token).safeApprove(address(_swapRouter), amountInMax); vars.amountIn = swapTokens( pathTokens, @@ -303,8 +307,8 @@ contract SwapAdapter is AccessControl { // Refund tokens if (vars.amountIn < amountInMax) { - IERC20(token).safeApprove(address(_swapRouter), 0); - IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + IERC20Upgradeable(token).safeApprove(address(_swapRouter), 0); + IERC20Upgradeable(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); } // Make Native Token deposit @@ -366,13 +370,19 @@ contract SwapAdapter is AccessControl { ); if (msg.value == 0) revert InsufficientAmount(msg.value); + vars.depositData = abi.encodePacked( + amountOut, + vars.depositDataAfterAmount + ); + vars.feeHandlerRouter = _bridge._feeHandler(); + (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( address(this), _bridge._domainID(), destinationDomainID, vars.resourceID, - abi.encodePacked(msg.value, vars.depositDataAfterAmount), + vars.depositData, "" // feeData - not parsed ); @@ -390,13 +400,8 @@ contract SwapAdapter is AccessControl { ); IPeripheryPayments(address(_swapRouter)).refundETH(); - vars.depositData = abi.encodePacked( - amountOut, - vars.depositDataAfterAmount - ); - vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); - IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); + IERC20Upgradeable(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); // Return unspent native currency to msg.sender @@ -455,5 +460,11 @@ contract SwapAdapter is AccessControl { path = abi.encodePacked(path, tokens[tokens.length - 1]); } + function _authorizeUpgrade(address newImplementation) + internal + onlyOwner + override + {} + receive() external payable {} } From 36d4185750ce0b45102ec53567602e18bb3dad64 Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 5 Nov 2024 15:45:41 +0200 Subject: [PATCH 21/28] Undo upgradeable --- contracts/adapters/SwapAdapter.sol | 53 +++++++++++------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 16a0a862..8802c70f 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -2,10 +2,9 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity 0.8.11; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../contracts/interfaces/IBridge.sol"; import "../../contracts/interfaces/IFeeHandler.sol"; import "../../contracts/adapters/interfaces/INativeTokenAdapter.sol"; @@ -18,9 +17,9 @@ import "../../contracts/adapters/interfaces/IPeripheryPayments.sol"; and then makes a deposit to the Bridge. @author ChainSafe Systems. */ -contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { +contract SwapAdapter is AccessControl { - using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeERC20 for IERC20; IBridge public immutable _bridge; address public immutable _weth; @@ -57,28 +56,26 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { event TokenResourceIDSet(address token, bytes32 resourceID); event TokensSwapped(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut); - /// @custom:oz-upgrades-unsafe-allow constructor constructor( IBridge bridge, address weth, IV3SwapRouter swapRouter, INativeTokenAdapter nativeTokenAdapter ) { - _disableInitializers(); _bridge = bridge; _weth = weth; _swapRouter = swapRouter; _nativeTokenAdapter = nativeTokenAdapter; + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } - - function initialize() public initializer { - _transferOwnership(msg.sender); - __UUPSUpgradeable_init(); + modifier onlyAdmin() { + if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert CallerNotAdmin(); + _; } // Admin functions - function setTokenResourceID(address token, bytes32 resourceID) external onlyOwner { + function setTokenResourceID(address token, bytes32 resourceID) external onlyAdmin { if (tokenToResourceID[token] == resourceID) revert AlreadySet(); tokenToResourceID[token] = resourceID; emit TokenResourceIDSet(token, resourceID); @@ -128,8 +125,8 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { vars.totalAmountOut = amountOut + vars.fee; // Swap tokens to ETH (exact output) - IERC20Upgradeable(token).safeTransferFrom(msg.sender, address(this), amountInMax); - IERC20Upgradeable(token).safeApprove(address(_swapRouter), amountInMax); + IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); + IERC20(token).safeApprove(address(_swapRouter), amountInMax); vars.amountIn = swapTokens( pathTokens, @@ -148,8 +145,8 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { // Refund tokens if (vars.amountIn < amountInMax) { - IERC20Upgradeable(token).safeApprove(address(_swapRouter), 0); - IERC20Upgradeable(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + IERC20(token).safeApprove(address(_swapRouter), 0); + IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); } // Return unspent fee to msg.sender @@ -191,7 +188,6 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { recipient ); if (msg.value == 0) revert InsufficientAmount(msg.value); - vars.depositData = abi.encodePacked( amountOut, vars.depositDataAfterAmount @@ -221,9 +217,8 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { ); IPeripheryPayments(address(_swapRouter)).refundETH(); - vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); - IERC20Upgradeable(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); + IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); // Return unspent native currency to msg.sender @@ -290,8 +285,8 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { vars.totalAmountOut = amountOut + vars.fee; // Swap tokens to ETH (exact output) - IERC20Upgradeable(token).safeTransferFrom(msg.sender, address(this), amountInMax); - IERC20Upgradeable(token).safeApprove(address(_swapRouter), amountInMax); + IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); + IERC20(token).safeApprove(address(_swapRouter), amountInMax); vars.amountIn = swapTokens( pathTokens, @@ -307,8 +302,8 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { // Refund tokens if (vars.amountIn < amountInMax) { - IERC20Upgradeable(token).safeApprove(address(_swapRouter), 0); - IERC20Upgradeable(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + IERC20(token).safeApprove(address(_swapRouter), 0); + IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); } // Make Native Token deposit @@ -369,14 +364,12 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { message ); if (msg.value == 0) revert InsufficientAmount(msg.value); - vars.depositData = abi.encodePacked( amountOut, vars.depositDataAfterAmount ); vars.feeHandlerRouter = _bridge._feeHandler(); - (vars.fee, ) = IFeeHandler(vars.feeHandlerRouter).calculateFee( address(this), _bridge._domainID(), @@ -401,7 +394,7 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { IPeripheryPayments(address(_swapRouter)).refundETH(); vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); - IERC20Upgradeable(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); + IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); // Return unspent native currency to msg.sender @@ -460,11 +453,5 @@ contract SwapAdapter is Initializable, OwnableUpgradeable, UUPSUpgradeable { path = abi.encodePacked(path, tokens[tokens.length - 1]); } - function _authorizeUpgrade(address newImplementation) - internal - onlyOwner - override - {} - receive() external payable {} } From 13a6471fece197f7491a941c3028fd301be622a1 Mon Sep 17 00:00:00 2001 From: viatrix Date: Thu, 7 Nov 2024 18:42:07 +0200 Subject: [PATCH 22/28] Use UniversalRouter --- contracts/adapters/SwapAdapter.sol | 71 +++++++++++-------- .../interfaces/IPeripheryPayments.sol | 28 -------- contracts/adapters/interfaces/IPermit2.sol | 15 ++++ .../adapters/interfaces/IUniversalRouter.sol | 11 +++ .../adapters/interfaces/IV3SwapRouter.sol | 67 ----------------- contracts/adapters/interfaces/IWETH.sol | 1 + testUnderForked/swapAdapter.js | 14 ++-- 7 files changed, 76 insertions(+), 131 deletions(-) delete mode 100644 contracts/adapters/interfaces/IPeripheryPayments.sol create mode 100644 contracts/adapters/interfaces/IPermit2.sol create mode 100644 contracts/adapters/interfaces/IUniversalRouter.sol delete mode 100644 contracts/adapters/interfaces/IV3SwapRouter.sol diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 8802c70f..821d466d 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -9,8 +9,8 @@ import "../../contracts/interfaces/IBridge.sol"; import "../../contracts/interfaces/IFeeHandler.sol"; import "../../contracts/adapters/interfaces/INativeTokenAdapter.sol"; import "../../contracts/adapters/interfaces/IWETH.sol"; -import "../../contracts/adapters/interfaces/IV3SwapRouter.sol"; -import "../../contracts/adapters/interfaces/IPeripheryPayments.sol"; +import "../../contracts/adapters/interfaces/IUniversalRouter.sol"; +import "../../contracts/adapters/interfaces/IPermit2.sol"; /** @title Contract that swaps tokens to ETH or ETH to tokens using Uniswap @@ -23,8 +23,9 @@ contract SwapAdapter is AccessControl { IBridge public immutable _bridge; address public immutable _weth; - IV3SwapRouter public immutable _swapRouter; + IUniversalRouter public immutable _swapRouter; INativeTokenAdapter public immutable _nativeTokenAdapter; + IPermit2 public immutable _permit2; mapping(address => bytes32) public tokenToResourceID; @@ -40,7 +41,6 @@ contract SwapAdapter is AccessControl { uint256 leftover; bytes depositDataAfterAmount; bytes path; - IV3SwapRouter.ExactInputParams params; bytes depositData; } @@ -56,16 +56,25 @@ contract SwapAdapter is AccessControl { event TokenResourceIDSet(address token, bytes32 resourceID); event TokensSwapped(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut); + /** + @dev The contract uses Uniswap UniversalRouter and Permit2. These addresses + should be set during initialization. Addresses of deployed Uniswap contracts can be + found in Uniswap docs. + */ + constructor( IBridge bridge, address weth, - IV3SwapRouter swapRouter, + IUniversalRouter swapRouter, + IPermit2 permit2, INativeTokenAdapter nativeTokenAdapter ) { _bridge = bridge; _weth = weth; _swapRouter = swapRouter; + _permit2 = permit2; _nativeTokenAdapter = nativeTokenAdapter; + IERC20(_weth).approve(address(_permit2), type(uint256).max); _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -78,6 +87,7 @@ contract SwapAdapter is AccessControl { function setTokenResourceID(address token, bytes32 resourceID) external onlyAdmin { if (tokenToResourceID[token] == resourceID) revert AlreadySet(); tokenToResourceID[token] = resourceID; + IERC20(token).approve(address(_permit2), type(uint256).max); emit TokenResourceIDSet(token, resourceID); } @@ -126,7 +136,6 @@ contract SwapAdapter is AccessControl { // Swap tokens to ETH (exact output) IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); - IERC20(token).safeApprove(address(_swapRouter), amountInMax); vars.amountIn = swapTokens( pathTokens, @@ -134,8 +143,7 @@ contract SwapAdapter is AccessControl { token, _weth, amountInMax, - vars.totalAmountOut, - 0 + vars.totalAmountOut ); IWETH(_weth).withdraw(vars.totalAmountOut); @@ -145,8 +153,7 @@ contract SwapAdapter is AccessControl { // Refund tokens if (vars.amountIn < amountInMax) { - IERC20(token).safeApprove(address(_swapRouter), 0); - IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this))); } // Return unspent fee to msg.sender @@ -206,17 +213,17 @@ contract SwapAdapter is AccessControl { if (msg.value < vars.fee) revert MsgValueLowerThanFee(msg.value); // Convert everything except the fee vars.swapAmount = msg.value - vars.fee; + IWETH(_weth).deposit{value: vars.swapAmount}(); vars.amountIn = swapTokens( pathTokens, pathFees, _weth, token, vars.swapAmount, - amountOut, - vars.swapAmount + amountOut ); - IPeripheryPayments(address(_swapRouter)).refundETH(); + IWETH(_weth).withdraw(IERC20(_weth).balanceOf(address(this))); vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); _bridge.deposit{value: vars.fee}(destinationDomainID, vars.resourceID, vars.depositData, ""); @@ -286,7 +293,6 @@ contract SwapAdapter is AccessControl { // Swap tokens to ETH (exact output) IERC20(token).safeTransferFrom(msg.sender, address(this), amountInMax); - IERC20(token).safeApprove(address(_swapRouter), amountInMax); vars.amountIn = swapTokens( pathTokens, @@ -294,16 +300,14 @@ contract SwapAdapter is AccessControl { token, _weth, amountInMax, - vars.totalAmountOut, - 0 + vars.totalAmountOut ); IWETH(_weth).withdraw(vars.totalAmountOut); // Refund tokens if (vars.amountIn < amountInMax) { - IERC20(token).safeApprove(address(_swapRouter), 0); - IERC20(token).safeTransfer(msg.sender, amountInMax - vars.amountIn); + IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this))); } // Make Native Token deposit @@ -382,16 +386,16 @@ contract SwapAdapter is AccessControl { if (msg.value < vars.fee) revert MsgValueLowerThanFee(msg.value); // Convert everything except the fee vars.swapAmount = msg.value - vars.fee; + IWETH(_weth).deposit{value: vars.swapAmount}(); vars.amountIn = swapTokens( pathTokens, pathFees, _weth, token, vars.swapAmount, - amountOut, - vars.swapAmount + amountOut ); - IPeripheryPayments(address(_swapRouter)).refundETH(); + IWETH(_weth).withdraw(IERC20(_weth).balanceOf(address(this))); vars.ERC20HandlerAddress = _bridge._resourceIDToHandlerAddress(vars.resourceID); IERC20(token).safeApprove(address(vars.ERC20HandlerAddress), amountOut); @@ -411,23 +415,28 @@ contract SwapAdapter is AccessControl { address tokenIn, address tokenOut, uint256 amountInMaximum, - uint256 amountOut, - uint256 valueToSend + uint256 amountOut ) internal returns(uint256 amountIn) { + uint256 balanceBefore = IERC20(tokenIn).balanceOf(address(this)); bytes memory path = _verifyAndEncodePath( pathTokens, pathFees, tokenIn, tokenOut ); - IV3SwapRouter.ExactOutputParams memory params = IV3SwapRouter.ExactOutputParams({ - path: path, - recipient: address(this), - amountOut: amountOut, - amountInMaximum: amountInMaximum - }); - - amountIn = _swapRouter.exactOutput{value: valueToSend}(params); + IPermit2(_permit2).approve(tokenIn, address(_swapRouter), uint160(amountInMaximum), uint48(block.timestamp)); + bytes memory commands = abi.encodePacked(uint8(0x01)); // V3_SWAP_EXACT_OUT + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode( + address(this), // The recipient of the output of the trade + amountOut, // The amount of output tokens to receive + amountInMaximum, // The maximum number of input tokens that should be spent + path, // The UniswapV3 encoded path to trade along + true // A flag for whether the input tokens should come from the msg.sender + ); + _swapRouter.execute(commands, inputs, block.timestamp); + uint256 balanceAfter = IERC20(tokenIn).balanceOf(address(this)); + amountIn = balanceBefore - balanceAfter; emit TokensSwapped(tokenIn, tokenOut, amountIn, amountOut); } diff --git a/contracts/adapters/interfaces/IPeripheryPayments.sol b/contracts/adapters/interfaces/IPeripheryPayments.sol deleted file mode 100644 index ac74f8c9..00000000 --- a/contracts/adapters/interfaces/IPeripheryPayments.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; - -/// @title Periphery Payments -/// @notice Functions to ease deposits and withdrawals of ETH -interface IPeripheryPayments { - /// @notice Unwraps the contract's WETH9 balance and sends it to recipient as ETH. - /// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users. - /// @param amountMinimum The minimum amount of WETH9 to unwrap - /// @param recipient The address receiving ETH - function unwrapWETH9(uint256 amountMinimum, address recipient) external payable; - - /// @notice Refunds any ETH balance held by this contract to the `msg.sender` - /// @dev Useful for bundling with mint or increase liquidity that uses ether, or exact output swaps - /// that use ether for the input amount - function refundETH() external payable; - - /// @notice Transfers the full amount of a token held by this contract to recipient - /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users - /// @param token The contract address of the token which will be transferred to `recipient` - /// @param amountMinimum The minimum amount of token required for a transfer - /// @param recipient The destination address of the token - function sweepToken( - address token, - uint256 amountMinimum, - address recipient - ) external payable; -} diff --git a/contracts/adapters/interfaces/IPermit2.sol b/contracts/adapters/interfaces/IPermit2.sol new file mode 100644 index 00000000..e4c023fc --- /dev/null +++ b/contracts/adapters/interfaces/IPermit2.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.11; + +/// @notice Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer. +/// @dev Users must approve Permit2 before calling any of the transfer functions. +interface IPermit2 { + /// @notice Approves the spender to use up to amount of the specified token up until the expiration + /// @param token The token to approve + /// @param spender The spender address to approve + /// @param amount The approved amount of the token + /// @param expiration The timestamp at which the approval is no longer valid + /// @dev The packed allowance also holds a nonce, which will stay unchanged in approve + /// @dev Setting amount to type(uint160).max sets an unlimited approval + function approve(address token, address spender, uint160 amount, uint48 expiration) external; +} \ No newline at end of file diff --git a/contracts/adapters/interfaces/IUniversalRouter.sol b/contracts/adapters/interfaces/IUniversalRouter.sol new file mode 100644 index 00000000..1c171401 --- /dev/null +++ b/contracts/adapters/interfaces/IUniversalRouter.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.11; + +interface IUniversalRouter { + + /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired. + /// @param commands A set of concatenated commands, each 1 byte in length + /// @param inputs An array of byte strings containing abi encoded inputs for each command + /// @param deadline The deadline by which the transaction must be executed + function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable; +} \ No newline at end of file diff --git a/contracts/adapters/interfaces/IV3SwapRouter.sol b/contracts/adapters/interfaces/IV3SwapRouter.sol deleted file mode 100644 index 063055f7..00000000 --- a/contracts/adapters/interfaces/IV3SwapRouter.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -/// @title Router token swapping functionality -/// @notice Functions for swapping tokens via Uniswap V3 -interface IV3SwapRouter { - struct ExactInputSingleParams { - address tokenIn; - address tokenOut; - uint24 fee; - address recipient; - uint256 amountIn; - uint256 amountOutMinimum; - uint160 sqrtPriceLimitX96; - } - - /// @notice Swaps `amountIn` of one token for as much as possible of another token - /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, - /// and swap the entire amount, enabling contracts to send tokens before calling this function. - /// @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 amountIn; - uint256 amountOutMinimum; - } - - /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path - /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, - /// and swap the entire amount, enabling contracts to send tokens before calling this function. - /// @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); - - struct ExactOutputSingleParams { - address tokenIn; - address tokenOut; - uint24 fee; - address recipient; - uint256 amountOut; - uint256 amountInMaximum; - uint160 sqrtPriceLimitX96; - } - - /// @notice Swaps as little as possible of one token for `amountOut` of another token - /// that may remain in the router after the swap. - /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata - /// @return amountIn The amount of the input token - function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); - - struct ExactOutputParams { - bytes path; - address recipient; - uint256 amountOut; - uint256 amountInMaximum; - } - - /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) - /// that may remain in the router after the swap. - /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata - /// @return amountIn The amount of the input token - function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); -} \ No newline at end of file diff --git a/contracts/adapters/interfaces/IWETH.sol b/contracts/adapters/interfaces/IWETH.sol index 2ea51f40..e42225ba 100644 --- a/contracts/adapters/interfaces/IWETH.sol +++ b/contracts/adapters/interfaces/IWETH.sol @@ -2,4 +2,5 @@ pragma solidity 0.8.11; interface IWETH { function withdraw(uint wad) external; + function deposit() external payable; } diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index df7863bb..6106e04d 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -20,7 +20,7 @@ const ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); contract("SwapAdapter", async (accounts) => { // Fee handler is mocked by BasicFeeHandler // deploy bridge, ERC20Handler, NativeTokenHandler, BasicFeeHandler, SwapAdapter - // use SwapRouter, USDC, WETH, user with USDC, user with ETH from mainnet fork + // use Uniswap UniversalRouter, Permit2, USDC, WETH, user with USDC from mainnet fork const recipientAddress = accounts[2]; const fee = 1000; const depositorAddress = accounts[3]; @@ -32,7 +32,8 @@ contract("SwapAdapter", async (accounts) => { const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; - const UNISWAP_SWAP_ROUTER_ADDRESS = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; + const UNIVERSAL_ROUTER_ADDRESS = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"; + const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; const resourceID_USDC = Helpers.createResourceID( USDC_ADDRESS, originDomainID @@ -85,7 +86,8 @@ contract("SwapAdapter", async (accounts) => { SwapAdapterInstance = await SwapAdapterContract.new( BridgeInstance.address, WETH_ADDRESS, - UNISWAP_SWAP_ROUTER_ADDRESS, + UNIVERSAL_ROUTER_ADDRESS, + PERMIT2_ADDRESS, NativeTokenAdapterInstance.address ); usdc = await ERC20MintableContract.at(USDC_ADDRESS); @@ -178,6 +180,7 @@ contract("SwapAdapter", async (accounts) => { const balanceAfter = await usdc.balanceOf(USDC_OWNER_ADDRESS); expect(balanceAfter.toNumber()).to.eq(balanceBefore - amountIn); expect(balanceAfter.toNumber()).to.be.gt(balanceBefore - amountInMax); + expect(balanceAfter.toNumber()).to.eq(balanceBefore - amountIn); const depositData = await Helpers.createERCDepositData(amountOut, 20, recipientAddress); @@ -218,8 +221,8 @@ contract("SwapAdapter", async (accounts) => { expect(await web3.eth.getBalance(BridgeInstance.address)).to.eq("0"); expect(await web3.eth.getBalance(FeeHandlerRouterInstance.address)).to.eq("0"); expect(await web3.eth.getBalance(ERC20HandlerInstance.address)).to.eq("0"); - expect(await web3.eth.getBalance(UNISWAP_SWAP_ROUTER_ADDRESS)).to.eq("0"); - expect((await weth.balanceOf(UNISWAP_SWAP_ROUTER_ADDRESS)).toNumber()).to.eq(0); + expect(await web3.eth.getBalance(UNIVERSAL_ROUTER_ADDRESS)).to.eq("0"); + expect((await weth.balanceOf(UNIVERSAL_ROUTER_ADDRESS)).toNumber()).to.eq(0); expect((await weth.balanceOf(SwapAdapterInstance.address)).toNumber()).to.eq(0); expect((await usdc.balanceOf(ERC20HandlerInstance.address)).toString()).to.eq(amountOut.toString()); @@ -300,6 +303,7 @@ contract("SwapAdapter", async (accounts) => { const balanceAfter = await usdc.balanceOf(USDC_OWNER_ADDRESS); expect(balanceAfter.toNumber()).to.eq(balanceBefore - amountIn); expect(balanceAfter.toNumber()).to.be.gt(balanceBefore - amountInMax); + expect(balanceAfter.toNumber()).to.eq(balanceBefore - amountIn); const depositData = await Helpers.createOptionalContractCallDepositData( amountOut, From ffd579df47f17f2197379a4bff0694e83aaf316a Mon Sep 17 00:00:00 2001 From: viatrix Date: Fri, 8 Nov 2024 17:02:24 +0200 Subject: [PATCH 23/28] Add constant for swap command --- contracts/adapters/SwapAdapter.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 821d466d..87b430ab 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -21,6 +21,8 @@ contract SwapAdapter is AccessControl { using SafeERC20 for IERC20; + uint8 V3_SWAP_EXACT_OUT = 1; + IBridge public immutable _bridge; address public immutable _weth; IUniversalRouter public immutable _swapRouter; @@ -425,7 +427,7 @@ contract SwapAdapter is AccessControl { tokenOut ); IPermit2(_permit2).approve(tokenIn, address(_swapRouter), uint160(amountInMaximum), uint48(block.timestamp)); - bytes memory commands = abi.encodePacked(uint8(0x01)); // V3_SWAP_EXACT_OUT + bytes memory commands = abi.encodePacked(V3_SWAP_EXACT_OUT); // V3_SWAP_EXACT_OUT bytes[] memory inputs = new bytes[](1); inputs[0] = abi.encode( address(this), // The recipient of the output of the trade From b30cabd2960c0586aa0cacaaa5955fc50ae953a9 Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Fri, 8 Nov 2024 22:43:42 +0700 Subject: [PATCH 24/28] Update contracts/adapters/SwapAdapter.sol --- contracts/adapters/SwapAdapter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 87b430ab..b3d9a5ef 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -21,7 +21,7 @@ contract SwapAdapter is AccessControl { using SafeERC20 for IERC20; - uint8 V3_SWAP_EXACT_OUT = 1; + uint8 constant V3_SWAP_EXACT_OUT = 1; IBridge public immutable _bridge; address public immutable _weth; From 9df8b947ade308899c3048343d62e903876d3d1e Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Thu, 14 Nov 2024 16:01:56 +0700 Subject: [PATCH 25/28] fix: Update SwapAdapter.sol toEth (#282) --- contracts/adapters/SwapAdapter.sol | 5 +++-- contracts/adapters/interfaces/INativeTokenAdapter.sol | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index b3d9a5ef..47d96258 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -27,6 +27,7 @@ contract SwapAdapter is AccessControl { address public immutable _weth; IUniversalRouter public immutable _swapRouter; INativeTokenAdapter public immutable _nativeTokenAdapter; + bytes32 public immutable _nativeResourceID; IPermit2 public immutable _permit2; mapping(address => bytes32) public tokenToResourceID; @@ -76,6 +77,7 @@ contract SwapAdapter is AccessControl { _swapRouter = swapRouter; _permit2 = permit2; _nativeTokenAdapter = nativeTokenAdapter; + _nativeResourceID = nativeTokenAdapter._resourceID(); IERC20(_weth).approve(address(_permit2), type(uint256).max); _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -115,8 +117,7 @@ contract SwapAdapter is AccessControl { uint24[] calldata pathFees ) external { LocalVars memory vars; - vars.resourceID = tokenToResourceID[token]; - if (vars.resourceID == bytes32(0)) revert TokenInvalid(); + vars.resourceID = _nativeResourceID; // Compose depositData vars.depositDataAfterAmount = abi.encodePacked( diff --git a/contracts/adapters/interfaces/INativeTokenAdapter.sol b/contracts/adapters/interfaces/INativeTokenAdapter.sol index 48fc769a..a937998e 100644 --- a/contracts/adapters/interfaces/INativeTokenAdapter.sol +++ b/contracts/adapters/interfaces/INativeTokenAdapter.sol @@ -7,6 +7,8 @@ pragma solidity 0.8.11; @author ChainSafe Systems. */ interface INativeTokenAdapter { + function _resourceID() external view returns(bytes32); + function depositToEVM( uint8 destinationDomainID, address recipientAddress From c4a8ffead00a03e3bbfc5292a4945b05d18224fa Mon Sep 17 00:00:00 2001 From: viatrix Date: Thu, 14 Nov 2024 11:38:49 +0200 Subject: [PATCH 26/28] Fix test --- testUnderForked/swapAdapter.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/testUnderForked/swapAdapter.js b/testUnderForked/swapAdapter.js index 6106e04d..69296b22 100644 --- a/testUnderForked/swapAdapter.js +++ b/testUnderForked/swapAdapter.js @@ -481,21 +481,23 @@ contract("SwapAdapter", async (accounts) => { }); it("should fail if the resource id is not configured", async () => { - const pathTokens = [WETH_ADDRESS, USDC_ADDRESS]; + const pathTokens = [USDC_ADDRESS, ]; const pathFees = [500]; - const amount = 1000000; - const amountOutMinimum = Ethers.utils.parseUnits("200000", "gwei"); + const amount = Ethers.utils.parseUnits("200000", "gwei"); + const amountOutMinimum = 1000000; await usdc.approve(SwapAdapterInstance.address, amount, {from: USDC_OWNER_ADDRESS}); await Helpers.expectToRevertWithCustomError( - SwapAdapterInstance.depositTokensToEth.call( + SwapAdapterInstance.depositEthToTokens.call( destinationDomainID, recipientAddress, USDC_ADDRESS, - amount, amountOutMinimum, pathTokens, pathFees, - {from: USDC_OWNER_ADDRESS} + { + from: USDC_OWNER_ADDRESS, + value: amount + } ), "TokenInvalid()" ); From 6c200713608b9b6f16169edbc43e67ca79e33649 Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 3 Dec 2024 15:48:33 +0200 Subject: [PATCH 27/28] Fix approve for swap --- contracts/adapters/SwapAdapter.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 47d96258..42783951 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -91,7 +91,6 @@ contract SwapAdapter is AccessControl { function setTokenResourceID(address token, bytes32 resourceID) external onlyAdmin { if (tokenToResourceID[token] == resourceID) revert AlreadySet(); tokenToResourceID[token] = resourceID; - IERC20(token).approve(address(_permit2), type(uint256).max); emit TokenResourceIDSet(token, resourceID); } @@ -270,8 +269,7 @@ contract SwapAdapter is AccessControl { uint24[] calldata pathFees ) external { LocalVars memory vars; - vars.resourceID = tokenToResourceID[token]; - if (vars.resourceID == bytes32(0)) revert TokenInvalid(); + vars.resourceID = _nativeResourceID; // Compose depositData vars.depositDataAfterAmount = abi.encodePacked( @@ -427,6 +425,7 @@ contract SwapAdapter is AccessControl { tokenIn, tokenOut ); + IERC20(tokenIn).approve(address(_permit2), amountInMaximum); IPermit2(_permit2).approve(tokenIn, address(_swapRouter), uint160(amountInMaximum), uint48(block.timestamp)); bytes memory commands = abi.encodePacked(V3_SWAP_EXACT_OUT); // V3_SWAP_EXACT_OUT bytes[] memory inputs = new bytes[](1); From e4d6cf34139f27fdc38ae6d55e6cd61fbf2052cf Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 3 Dec 2024 16:06:09 +0200 Subject: [PATCH 28/28] Update approve --- contracts/adapters/SwapAdapter.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/adapters/SwapAdapter.sol b/contracts/adapters/SwapAdapter.sol index 42783951..0c182caf 100644 --- a/contracts/adapters/SwapAdapter.sol +++ b/contracts/adapters/SwapAdapter.sol @@ -437,6 +437,7 @@ contract SwapAdapter is AccessControl { true // A flag for whether the input tokens should come from the msg.sender ); _swapRouter.execute(commands, inputs, block.timestamp); + IERC20(tokenIn).approve(address(_permit2), 0); uint256 balanceAfter = IERC20(tokenIn).balanceOf(address(this)); amountIn = balanceBefore - balanceAfter; emit TokensSwapped(tokenIn, tokenOut, amountIn, amountOut);