From 4510a86dfd23fe1a6b24f40244978d8fb489879c Mon Sep 17 00:00:00 2001 From: viatrix Date: Mon, 4 Nov 2024 21:38:42 +0200 Subject: [PATCH] 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");