From 7222235e922b114e62b1e3496a93d90b210f7e2e Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 20 Jul 2024 22:07:01 +0200 Subject: [PATCH 01/17] Add swap feature to AxelarHandler executeWithToken --- AxelarHandler/src/AxelarHandler.sol | 72 ++++++++++++++++---- AxelarHandler/test/AxelarHandler.t.sol | 94 +++++++++++++++++++++----- 2 files changed, 133 insertions(+), 33 deletions(-) diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index f308911..246b643 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -35,6 +35,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, error InsufficientNativeToken(); error ETHSendFailed(); error Reentrancy(); + error FunctionCodeNotSupported(); bytes32 private _wETHSymbolHash; @@ -363,26 +364,67 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, string calldata tokenSymbol, uint256 amount ) internal override { - (bool unwrap, address destination) = abi.decode(payload, (bool, address)); IERC20Upgradeable token = IERC20Upgradeable(_getTokenAddress(tokenSymbol)); - // If unwrap is set and the token can be unwrapped. - if (unwrap && _wETHSymbolHash != DISABLED_SYMBOL && keccak256(abi.encodePacked(tokenSymbol)) == _wETHSymbolHash) - { - // Unwrap native token. - IWETH weth = IWETH(address(token)); - weth.withdraw(amount); + (uint8 functionCode, bytes memory data) = abi.decode(payload, (uint8, bytes)); + if (functionCode == 0) { + (bool unwrap, address destination) = abi.decode(data, (bool, address)); + // If unwrap is set and the token can be unwrapped. + if ( + unwrap && _wETHSymbolHash != DISABLED_SYMBOL + && keccak256(abi.encodePacked(tokenSymbol)) == _wETHSymbolHash + ) { + // Unwrap native token. + IWETH weth = IWETH(address(token)); + weth.withdraw(amount); + + // Send it unwrapped to the destination + (bool success,) = destination.call{value: amount}(""); + + if (!success) { + revert NativePaymentFailed(); + } + } + // Just send the tokens received to the destination. + else { + token.safeTransfer(destination, amount); + } + } else if (functionCode == 1) { + (address destination, address outputTokenAddr, bytes memory swapCalldata) = + abi.decode(data, (address, address, bytes)); + + IERC20Upgradeable outputToken = IERC20Upgradeable(outputTokenAddr); + + // Approve the swap router to spend the input tokens + token.safeApprove(swapRouter, amount); + + // Get the contract's balances previous to the swap + uint256 preInputBalance = token.balanceOf(address(this)); + uint256 preOutputBalance = outputToken.balanceOf(address(this)); + + // Call the swap router and perform the swap + (bool success,) = swapRouter.call(swapCalldata); + if (!success) revert SwapFailed(); - // Send it unwrapped to the destination - (bool success,) = destination.call{value: amount}(""); + // Get the contract's balances after the swap + uint256 dust = token.balanceOf(address(this)) + amount - preInputBalance; + uint256 postOutputBalance = outputToken.balanceOf(address(this)); + + // Check that the contract's output token balance has increased + if (preOutputBalance >= postOutputBalance) revert InsufficientSwapOutput(); + uint256 outputAmount = postOutputBalance - preOutputBalance; + + outputToken.transfer(destination, outputAmount); - if (!success) { - revert NativePaymentFailed(); + // Refund the remaining amount + if (dust != 0) { + token.transfer(destination, dust); + + // Revoke approval + token.safeApprove(swapRouter, 0); } - } - // Just send the tokens received to the destination. - else { - token.safeTransfer(destination, amount); + } else { + revert FunctionCodeNotSupported(); } } diff --git a/AxelarHandler/test/AxelarHandler.t.sol b/AxelarHandler/test/AxelarHandler.t.sol index e017218..19662d1 100644 --- a/AxelarHandler/test/AxelarHandler.t.sol +++ b/AxelarHandler/test/AxelarHandler.t.sol @@ -14,6 +14,8 @@ import {IERC20Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contract import {ERC1967Proxy} from "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract AxelarHandlerTest is Test { + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public immutable ALICE = makeAddr("ALICE"); address public immutable BOB = makeAddr("BOB"); @@ -257,7 +259,7 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(token)); - bytes memory payload = abi.encode(true, ALICE); + bytes memory payload = abi.encode(uint8(0), abi.encode(true, ALICE)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, 100 ether @@ -284,7 +286,7 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(token)); - bytes memory payload = abi.encode(false, ALICE); + bytes memory payload = abi.encode(uint8(0), abi.encode(false, ALICE)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, 100 ether @@ -312,7 +314,7 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(token)); - bytes memory payload = abi.encode(true, ALICE); + bytes memory payload = abi.encode(uint8(0), abi.encode(true, ALICE)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, 100 ether @@ -323,32 +325,89 @@ contract AxelarHandlerTest is Test { assertEq(ALICE.balance, 100 ether, "Alice native balance after"); } - function test_executeWithToken_nonunwrap_WETH() public { + function test_executeWithToken_swap() public { + uint256 inputAmount = 5 ether; + uint256 minOutput = 10_000 * 1e6; + string memory symbol = "WETH"; - IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); - vm.label(address(token), "WETH"); + IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + vm.label(address(inputToken), "WETH"); - deal(address(token), address(this), 100 ether); - token.transfer(address(handler), 100 ether); + IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); + vm.label(address(inputToken), "USDC"); - assertEq(token.balanceOf(address(handler)), 100 ether, "Handler balance before"); + deal(address(inputToken), address(this), inputAmount); + inputToken.transfer(address(handler), inputAmount); - assertEq(token.balanceOf(ALICE), 0, "Alice token balance before"); - assertEq(ALICE.balance, 0, "Alice native balance before"); + assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); + assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); deployCodeTo("MockGateway.sol", address(gateway)); MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, address(token)); + mockGateway.saveTokenAddress(symbol, address(inputToken)); - bytes memory payload = abi.encode(false, ALICE); + address[] memory path = new address[](2); + path[0] = address(inputToken); + path[1] = address(outputToken); + bytes memory swapCalldata = + abi.encodeWithSelector(bytes4(0x472b43f3), inputAmount, minOutput, path, address(handler)); + bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE, address(outputToken), swapCalldata)); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, 100 ether + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount ); - assertEq(token.balanceOf(address(handler)), 0, "Handler balance after"); - assertEq(token.balanceOf(ALICE), 100 ether, "Alice token balance after"); - assertEq(ALICE.balance, 0, "Alice native balance after"); + assertEq(inputToken.balanceOf(ALICE), 0, "User got refunded input"); + assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); + assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); + assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + } + + function test_executeWithToken_swap_refundDust() public { + uint256 inputAmount = 4.95 ether; + uint256 dust = 0.05 ether; + uint256 minOutput = 10_000 * 1e6; + + string memory symbol = "WETH"; + IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + vm.label(address(inputToken), "WETH"); + + IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); + vm.label(address(inputToken), "USDC"); + + deal(address(inputToken), address(this), inputAmount); + inputToken.transfer(address(handler), inputAmount); + + assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); + assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + + deployCodeTo("MockGateway.sol", address(gateway)); + MockGateway mockGateway = MockGateway(address(gateway)); + mockGateway.saveTokenAddress(symbol, address(inputToken)); + + address[] memory path = new address[](2); + path[0] = address(inputToken); + path[1] = address(outputToken); + bytes memory swapCalldata = + abi.encodeWithSelector(bytes4(0x472b43f3), inputAmount - dust, minOutput, path, address(handler)); + bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE, address(outputToken), swapCalldata)); + + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + ); + + assertEq(inputToken.balanceOf(ALICE), dust, "User didn't got dust refunded"); + assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); + assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); + assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); } function test_swapAndGmpTransferERC20Token_ETH( @@ -365,7 +424,6 @@ contract AxelarHandlerTest is Test { assertEq(address(handler).balance, 0, "Pre-swap contract balance"); vm.deal(ALICE, amount + gasAmount); - uint256 minAmount = 0.05 * 1e8; address[] memory path = new address[](2); path[0] = gateway.tokenAddresses("WETH"); From 4e7761f53c4699d00606818a4c7ff2525acb0615 Mon Sep 17 00:00:00 2001 From: Manu Date: Sun, 28 Jul 2024 02:08:46 +0200 Subject: [PATCH 02/17] Fix scripts naming --- AxelarHandler/script/AxelarHandlerSetRouter.sol | 2 +- CCTPRelayer/script/SetRouter.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AxelarHandler/script/AxelarHandlerSetRouter.sol b/AxelarHandler/script/AxelarHandlerSetRouter.sol index 2b78d7f..5c85fe4 100644 --- a/AxelarHandler/script/AxelarHandlerSetRouter.sol +++ b/AxelarHandler/script/AxelarHandlerSetRouter.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import {AxelarHandler} from "src/AxelarHandler.sol"; -contract AxelarHandlerUpgradeScript is Script { +contract AxelarHandlerSetRouterScript is Script { AxelarHandler public handler; address public router; diff --git a/CCTPRelayer/script/SetRouter.sol b/CCTPRelayer/script/SetRouter.sol index 5aaa412..7530e9e 100644 --- a/CCTPRelayer/script/SetRouter.sol +++ b/CCTPRelayer/script/SetRouter.sol @@ -5,7 +5,7 @@ import "./BaseScript.sol"; import {ERC1967Proxy} from "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {CCTPRelayer} from "src/CCTPRelayer.sol"; -contract DeploymentScript is BaseScript { +contract SetRouterScript is BaseScript { function run() public { vm.startBroadcast(); From 89ba944d6c4711443856f55ba4b46d4ad3dd6f46 Mon Sep 17 00:00:00 2001 From: Manu Date: Sun, 28 Jul 2024 20:06:35 +0200 Subject: [PATCH 03/17] New swap logic code for executeWithToken --- AxelarHandler/src/AxelarHandler.sol | 208 +++++++++++++----- .../src/interfaces/ISwapRouter02.sol | 122 ++++++++++ 2 files changed, 275 insertions(+), 55 deletions(-) create mode 100644 AxelarHandler/src/interfaces/ISwapRouter02.sol diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index 246b643..327c846 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -14,6 +14,8 @@ import {Ownable2StepUpgradeable} from import {UUPSUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {ISwapRouter02} from "./interfaces/ISwapRouter02.sol"; + /// @title AxelarHandler /// @notice allows to send and receive tokens to/from other chains through axelar gateway while wrapping the native tokens. /// @author Skip Protocol. @@ -37,6 +39,17 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, error Reentrancy(); error FunctionCodeNotSupported(); + enum Commands { + SendToken, + SendNative, + ExactInputSingleSwap, + ExactInputSwap, + ExactOutputSingleSwap, + ExactOutputSwap, + ExactTokensForTokensSwap, + TokensForExactTokensSwap + } + bytes32 private _wETHSymbolHash; string public wETHSymbol; @@ -46,7 +59,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, bytes32 public constant DISABLED_SYMBOL = keccak256(abi.encodePacked("DISABLED")); - address public swapRouter; + ISwapRouter02 public swapRouter; bool internal reentrant; @@ -84,7 +97,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, function setSwapRouter(address _swapRouter) external onlyOwner { if (_swapRouter == address(0)) revert ZeroAddress(); - swapRouter = _swapRouter; + swapRouter = ISwapRouter02(_swapRouter); } /// @notice Sends native currency to other chains through the axelar gateway. @@ -230,7 +243,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, uint256 preOutputBalance = outputToken.balanceOf(address(this)); // Call the swap router and perform the swap - (bool success,) = swapRouter.call{value: amount}(swapCalldata); + (bool success,) = address(swapRouter).call{value: amount}(swapCalldata); if (!success) revert SwapFailed(); // Get the contract's balances after the swap @@ -256,14 +269,14 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, token.safeTransferFrom(msg.sender, address(this), amount); // Approve the swap router to spend the input tokens - token.safeApprove(swapRouter, amount); + token.safeApprove(address(swapRouter), amount); // Get the contract's balances previous to the swap uint256 preInputBalance = token.balanceOf(address(this)); uint256 preOutputBalance = outputToken.balanceOf(address(this)); // Call the swap router and perform the swap - (bool success,) = swapRouter.call(swapCalldata); + (bool success,) = address(swapRouter).call(swapCalldata); if (!success) revert SwapFailed(); // Get the contract's balances after the swap @@ -279,7 +292,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, token.transfer(msg.sender, dust); // Revoke approval - token.safeApprove(swapRouter, 0); + token.safeApprove(address(swapRouter), 0); } } @@ -366,65 +379,150 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, ) internal override { IERC20Upgradeable token = IERC20Upgradeable(_getTokenAddress(tokenSymbol)); - (uint8 functionCode, bytes memory data) = abi.decode(payload, (uint8, bytes)); - if (functionCode == 0) { - (bool unwrap, address destination) = abi.decode(data, (bool, address)); - // If unwrap is set and the token can be unwrapped. - if ( - unwrap && _wETHSymbolHash != DISABLED_SYMBOL - && keccak256(abi.encodePacked(tokenSymbol)) == _wETHSymbolHash - ) { - // Unwrap native token. - IWETH weth = IWETH(address(token)); - weth.withdraw(amount); - - // Send it unwrapped to the destination - (bool success,) = destination.call{value: amount}(""); - - if (!success) { - revert NativePaymentFailed(); - } - } - // Just send the tokens received to the destination. - else { - token.safeTransfer(destination, amount); + (Commands command, bytes memory data) = abi.decode(payload, (Commands, bytes)); + if (command == Commands.SendToken) { + _sendToken(address(token), amount, data); + } else if (command == Commands.SendNative) { + if (_wETHSymbolHash != DISABLED_SYMBOL && keccak256(abi.encodePacked(tokenSymbol)) == _wETHSymbolHash) { + _sendNative(address(token), amount, data); + } else { + _sendToken(address(token), amount, data); } - } else if (functionCode == 1) { - (address destination, address outputTokenAddr, bytes memory swapCalldata) = - abi.decode(data, (address, address, bytes)); + } else if (command == Commands.ExactInputSingleSwap) { + _exactInputSingleSwap(address(token), amount, data); + } else if (command == Commands.ExactInputSwap) { + _exactInputSwap(address(token), amount, data); + } else if (command == Commands.ExactOutputSingleSwap) { + _exactOutputSingleSwap(address(token), amount, data); + } else if (command == Commands.ExactOutputSwap) { + _exactOutputSwap(address(token), amount, data); + } else if (command == Commands.ExactTokensForTokensSwap) { + _exactTokensForTokensSwap(address(token), amount, data); + } else if (command == Commands.TokensForExactTokensSwap) { + _tokensForExactTokensSwap(address(token), amount, data); + } else { + revert FunctionCodeNotSupported(); + } + } - IERC20Upgradeable outputToken = IERC20Upgradeable(outputTokenAddr); + function _sendToken(address token, uint256 amount, bytes memory data) internal { + address destination = abi.decode(data, (address)); - // Approve the swap router to spend the input tokens - token.safeApprove(swapRouter, amount); + IERC20Upgradeable(token).safeTransfer(destination, amount); + } - // Get the contract's balances previous to the swap - uint256 preInputBalance = token.balanceOf(address(this)); - uint256 preOutputBalance = outputToken.balanceOf(address(this)); + function _sendNative(address token, uint256 amount, bytes memory data) internal { + address destination = abi.decode(data, (address)); - // Call the swap router and perform the swap - (bool success,) = swapRouter.call(swapCalldata); - if (!success) revert SwapFailed(); + // Unwrap native token. + IWETH weth = IWETH(token); + weth.withdraw(amount); - // Get the contract's balances after the swap - uint256 dust = token.balanceOf(address(this)) + amount - preInputBalance; - uint256 postOutputBalance = outputToken.balanceOf(address(this)); + // Send it unwrapped to the destination + (bool success,) = destination.call{value: amount}(""); - // Check that the contract's output token balance has increased - if (preOutputBalance >= postOutputBalance) revert InsufficientSwapOutput(); - uint256 outputAmount = postOutputBalance - preOutputBalance; + if (!success) { + revert NativePaymentFailed(); + } + } - outputToken.transfer(destination, outputAmount); + function _exactInputSingleSwap(address token, uint256 amount, bytes memory data) internal { + ISwapRouter02.ExactInputSingleParams memory params; + params.tokenIn = token; + params.amountIn = amount; - // Refund the remaining amount - if (dust != 0) { - token.transfer(destination, dust); + (params.tokenOut, params.fee, params.recipient, params.amountOutMinimum, params.sqrtPriceLimitX96) = + abi.decode(data, (address, uint24, address, uint256, uint160)); - // Revoke approval - token.safeApprove(swapRouter, 0); - } - } else { - revert FunctionCodeNotSupported(); + uint256 preBal = _preSwap(token, amount); + swapRouter.exactInputSingle(params); + _postSwap(token, params.recipient, preBal); + } + + function _exactInputSwap(address token, uint256 amount, bytes memory data) internal { + ISwapRouter02.ExactInputParams memory params; + params.amountIn = amount; + + (params.path, params.recipient, params.amountOutMinimum) = abi.decode(data, (bytes, address, uint256)); + + // if (address(bytes20(params.path[:20])) != token) { + // revert("Test, not token in path"); + // } + + uint256 preBal = _preSwap(token, amount); + swapRouter.exactInput(params); + _postSwap(token, params.recipient, preBal); + } + + function _exactOutputSingleSwap(address token, uint256 amount, bytes memory data) internal { + ISwapRouter02.ExactOutputSingleParams memory params; + params.tokenIn = token; + params.amountInMaximum = amount; + + (params.tokenOut, params.fee, params.recipient, params.amountOut, params.sqrtPriceLimitX96) = + abi.decode(data, (address, uint24, address, uint256, uint160)); + + uint256 preBal = _preSwap(token, amount); + swapRouter.exactOutputSingle(params); + _postSwap(token, params.recipient, preBal); + } + + function _exactOutputSwap(address token, uint256 amount, bytes memory data) internal { + ISwapRouter02.ExactOutputParams memory params; + params.amountInMaximum = amount; + + (params.path, params.recipient, params.amountOut) = abi.decode(data, (bytes, address, uint256)); + + // if (address(bytes20(params.path[:20])) != token) { + // revert("Test, not token in path"); + // } + + uint256 preBal = _preSwap(token, amount); + swapRouter.exactOutput(params); + _postSwap(token, params.recipient, preBal); + } + + function _exactTokensForTokensSwap(address token, uint256 amount, bytes memory data) internal { + (uint256 amountOutMin, address[] memory path, address destination) = + abi.decode(data, (uint256, address[], address)); + + if (path[0] != token) { + revert("Test, not token in path"); + } + + uint256 preBal = _preSwap(token, amount); + swapRouter.swapExactTokensForTokens(amount, amountOutMin, path, destination); + _postSwap(token, destination, preBal); + } + + function _tokensForExactTokensSwap(address token, uint256 amount, bytes memory data) internal { + (uint256 amountOut, address[] memory path, address destination) = + abi.decode(data, (uint256, address[], address)); + + if (path[0] != token) { + revert("Test, not token in path"); + } + + uint256 preBal = _preSwap(token, amount); + swapRouter.swapExactTokensForTokens(amount, amountOut, path, destination); + _postSwap(token, destination, preBal); + } + + function _preSwap(address _tokenIn, uint256 amount) internal returns (uint256 preBal) { + IERC20Upgradeable tokenIn = IERC20Upgradeable(_tokenIn); + + preBal = tokenIn.balanceOf(address(this)) - amount; + + tokenIn.safeApprove(address(swapRouter), amount); + } + + function _postSwap(address _tokenIn, address destination, uint256 preBal) internal { + IERC20Upgradeable tokenIn = IERC20Upgradeable(_tokenIn); + + uint256 dust = tokenIn.balanceOf(address(this)) - preBal; + if (dust != 0) { + tokenIn.safeApprove(address(swapRouter), 0); + tokenIn.transfer(destination, dust); } } diff --git a/AxelarHandler/src/interfaces/ISwapRouter02.sol b/AxelarHandler/src/interfaces/ISwapRouter02.sol new file mode 100644 index 0000000..b053fe9 --- /dev/null +++ b/AxelarHandler/src/interfaces/ISwapRouter02.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.18; + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V2 +interface IV2SwapRouter { + /// @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 amountIn The amount of token to swap + /// @param amountOutMin The minimum amount of output that must be received + /// @param path The ordered list of tokens to swap through + /// @param to The recipient address + /// @return amountOut The amount of the received token + function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to) + external + payable + returns (uint256 amountOut); + + /// @notice Swaps as little as possible of one token for an exact amount of another token + /// @param amountOut The amount of token to swap for + /// @param amountInMax The maximum amount of input that the caller will pay + /// @param path The ordered list of tokens to swap through + /// @param to The recipient address + /// @return amountIn The amount of token to pay + function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to) + external + payable + returns (uint256 amountIn); +} + +/// @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); +} + +interface IMulticallExtended { + /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed + /// @dev The `msg.value` should not be trusted for any method callable from multicall. + /// @param data The encoded function data for each of the calls to make to this contract + /// @return results The results from each of the calls passed in via data + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); + + /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed + /// @dev The `msg.value` should not be trusted for any method callable from multicall. + /// @param deadline The time by which this function must be called before failing + /// @param data The encoded function data for each of the calls to make to this contract + /// @return results The results from each of the calls passed in via data + function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory results); + + /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed + /// @dev The `msg.value` should not be trusted for any method callable from multicall. + /// @param previousBlockhash The expected parent blockHash + /// @param data The encoded function data for each of the calls to make to this contract + /// @return results The results from each of the calls passed in via data + function multicall(bytes32 previousBlockhash, bytes[] calldata data) + external + payable + returns (bytes[] memory results); +} + +/// @title Router token swapping functionality +interface ISwapRouter02 is IV2SwapRouter, IV3SwapRouter, IMulticallExtended {} From e4348aa6acc7118f9caa23107d0ee5c42078d6b3 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 2 Aug 2024 12:51:31 +0100 Subject: [PATCH 04/17] Multi-hop swap functionality --- AxelarHandler/src/AxelarHandler.sol | 267 +++++++++++++++++-------- AxelarHandler/src/libraries/Path.sol | 159 +++++++++++++++ AxelarHandler/test/AxelarHandler.t.sol | 183 +++++++++++++---- 3 files changed, 493 insertions(+), 116 deletions(-) create mode 100644 AxelarHandler/src/libraries/Path.sol diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index 327c846..269808e 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -15,12 +15,14 @@ import {UUPSUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/ import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import {ISwapRouter02} from "./interfaces/ISwapRouter02.sol"; +import {Path} from "./libraries/Path.sol"; /// @title AxelarHandler /// @notice allows to send and receive tokens to/from other chains through axelar gateway while wrapping the native tokens. /// @author Skip Protocol. contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; + using Path for bytes; error EmptySymbol(); error NativeSentDoesNotMatchAmounts(); @@ -42,12 +44,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, enum Commands { SendToken, SendNative, - ExactInputSingleSwap, - ExactInputSwap, - ExactOutputSingleSwap, - ExactOutputSwap, - ExactTokensForTokensSwap, - TokensForExactTokensSwap + Swap } bytes32 private _wETHSymbolHash; @@ -377,43 +374,61 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, string calldata tokenSymbol, uint256 amount ) internal override { - IERC20Upgradeable token = IERC20Upgradeable(_getTokenAddress(tokenSymbol)); + address token = _getTokenAddress(tokenSymbol); + IERC20Upgradeable tokenIn = IERC20Upgradeable(token); (Commands command, bytes memory data) = abi.decode(payload, (Commands, bytes)); if (command == Commands.SendToken) { - _sendToken(address(token), amount, data); + address destination = abi.decode(data, (address)); + _sendToken(token, amount, destination); } else if (command == Commands.SendNative) { + address destination = abi.decode(data, (address)); if (_wETHSymbolHash != DISABLED_SYMBOL && keccak256(abi.encodePacked(tokenSymbol)) == _wETHSymbolHash) { - _sendNative(address(token), amount, data); + _sendNative(token, amount, destination); } else { - _sendToken(address(token), amount, data); + _sendToken(token, amount, destination); + } + } else if (command == Commands.Swap) { + (address destination, uint256 amountOutMin, bool unwrapOut, bytes[] memory swaps) = + abi.decode(data, (address, uint256, bool, bytes[])); + + uint256 length = swaps.length; + for (uint256 i; i < length; ++i) { + (uint8 swapFunction, bytes memory swapPayload) = abi.decode(swaps[i], (uint8, bytes)); + + if (swapFunction == uint8(0)) { + (token, amount) = _exactInputSingleSwap(token, destination, amount, swapPayload); + } else if (swapFunction == uint8(1)) { + (token, amount) = _exactInputSwap(token, destination, amount, swapPayload); + } else if (swapFunction == uint8(2)) { + (token, amount) = _exactTokensForTokensSwap(token, destination, amount, swapPayload); + } else if (swapFunction == uint8(3)) { + (token, amount) = _exactOutputSingleSwap(token, destination, amount, swapPayload); + } else if (swapFunction == uint8(4)) { + (token, amount) = _exactOutputSwap(token, destination, amount, swapPayload); + } else if (swapFunction == uint8(5)) { + (token, amount) = _tokensForExactTokensSwap(token, destination, amount, swapPayload); + } else { + revert FunctionCodeNotSupported(); + } + } + + if (amount < amountOutMin) revert InsufficientSwapOutput(); + if (unwrapOut && token == _getTokenAddress(wETHSymbol)) { + _sendNative(token, amount, destination); + } else { + _sendToken(token, amount, destination); } - } else if (command == Commands.ExactInputSingleSwap) { - _exactInputSingleSwap(address(token), amount, data); - } else if (command == Commands.ExactInputSwap) { - _exactInputSwap(address(token), amount, data); - } else if (command == Commands.ExactOutputSingleSwap) { - _exactOutputSingleSwap(address(token), amount, data); - } else if (command == Commands.ExactOutputSwap) { - _exactOutputSwap(address(token), amount, data); - } else if (command == Commands.ExactTokensForTokensSwap) { - _exactTokensForTokensSwap(address(token), amount, data); - } else if (command == Commands.TokensForExactTokensSwap) { - _tokensForExactTokensSwap(address(token), amount, data); } else { revert FunctionCodeNotSupported(); } } - function _sendToken(address token, uint256 amount, bytes memory data) internal { - address destination = abi.decode(data, (address)); - + function _sendToken(address token, uint256 amount, address destination) internal { IERC20Upgradeable(token).safeTransfer(destination, amount); } - function _sendNative(address token, uint256 amount, bytes memory data) internal { - address destination = abi.decode(data, (address)); - + function _sendNative(address token, uint256 amount, address destination) internal { // Unwrap native token. IWETH weth = IWETH(token); weth.withdraw(amount); @@ -426,103 +441,191 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, } } - function _exactInputSingleSwap(address token, uint256 amount, bytes memory data) internal { + function _exactInputSingleSwap(address token, address destination, uint256 amount, bytes memory data) + internal + returns (address tokenOut, uint256 amountOut) + { ISwapRouter02.ExactInputSingleParams memory params; + (tokenOut, params.fee, params.sqrtPriceLimitX96) = abi.decode(data, (address, uint24, uint160)); + params.tokenIn = token; + params.tokenOut = tokenOut; params.amountIn = amount; + params.recipient = address(this); + + IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); + IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); - (params.tokenOut, params.fee, params.recipient, params.amountOutMinimum, params.sqrtPriceLimitX96) = - abi.decode(data, (address, uint24, address, uint256, uint160)); + uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; + uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - uint256 preBal = _preSwap(token, amount); + tokenSwapIn.safeApprove(address(swapRouter), amount); swapRouter.exactInputSingle(params); - _postSwap(token, params.recipient, preBal); + + uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; + amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; + + if (dustIn != 0) { + tokenSwapIn.safeApprove(address(swapRouter), 0); + tokenSwapIn.safeTransfer(destination, dustIn); + } } - function _exactInputSwap(address token, uint256 amount, bytes memory data) internal { + function _exactInputSwap(address token, address destination, uint256 amount, bytes memory data) + internal + returns (address tokenOut, uint256 amountOut) + { ISwapRouter02.ExactInputParams memory params; + (params.path) = abi.decode(data, (bytes)); + + params.recipient = address(this); params.amountIn = amount; - (params.path, params.recipient, params.amountOutMinimum) = abi.decode(data, (bytes, address, uint256)); + (address tokenA,,) = params.path.decodeFirstPool(); + + if (tokenA != token) {} + + (, tokenOut,) = params.path.decodeLastPool(); + + IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); + IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); - // if (address(bytes20(params.path[:20])) != token) { - // revert("Test, not token in path"); - // } + uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; + uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - uint256 preBal = _preSwap(token, amount); + tokenSwapIn.safeApprove(address(swapRouter), amount); swapRouter.exactInput(params); - _postSwap(token, params.recipient, preBal); + + uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; + amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; + + if (dustIn != 0) { + tokenSwapIn.safeApprove(address(swapRouter), 0); + tokenSwapIn.safeTransfer(destination, dustIn); + } } - function _exactOutputSingleSwap(address token, uint256 amount, bytes memory data) internal { + function _exactOutputSingleSwap(address token, address destination, uint256 amount, bytes memory data) + internal + returns (address tokenOut, uint256 amountOut) + { ISwapRouter02.ExactOutputSingleParams memory params; + (tokenOut, amountOut, params.fee, params.sqrtPriceLimitX96) = + abi.decode(data, (address, uint256, uint24, uint160)); + params.tokenIn = token; + params.tokenOut = tokenOut; + params.recipient = address(this); + params.amountOut = amountOut; params.amountInMaximum = amount; - (params.tokenOut, params.fee, params.recipient, params.amountOut, params.sqrtPriceLimitX96) = - abi.decode(data, (address, uint24, address, uint256, uint160)); + IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); + IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + + uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; + uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - uint256 preBal = _preSwap(token, amount); + tokenSwapIn.safeApprove(address(swapRouter), amount); swapRouter.exactOutputSingle(params); - _postSwap(token, params.recipient, preBal); + + uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; + amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; + + if (dustIn != 0) { + tokenSwapIn.safeApprove(address(swapRouter), 0); + tokenSwapIn.safeTransfer(destination, dustIn); + } } - function _exactOutputSwap(address token, uint256 amount, bytes memory data) internal { + function _exactOutputSwap(address token, address destination, uint256 amount, bytes memory data) + internal + returns (address tokenOut, uint256 amountOut) + { ISwapRouter02.ExactOutputParams memory params; + (amountOut, params.path) = abi.decode(data, (uint256, bytes)); + + params.recipient = address(this); + params.amountOut = amountOut; params.amountInMaximum = amount; - (params.path, params.recipient, params.amountOut) = abi.decode(data, (bytes, address, uint256)); + (address tokenA,,) = params.path.decodeFirstPool(); + + if (tokenA != token) {} - // if (address(bytes20(params.path[:20])) != token) { - // revert("Test, not token in path"); - // } + (, tokenOut,) = params.path.decodeLastPool(); - uint256 preBal = _preSwap(token, amount); + IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); + IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + + uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; + uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); + + tokenSwapIn.safeApprove(address(swapRouter), amount); swapRouter.exactOutput(params); - _postSwap(token, params.recipient, preBal); - } - function _exactTokensForTokensSwap(address token, uint256 amount, bytes memory data) internal { - (uint256 amountOutMin, address[] memory path, address destination) = - abi.decode(data, (uint256, address[], address)); + uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; + amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - if (path[0] != token) { - revert("Test, not token in path"); + if (dustIn != 0) { + tokenSwapIn.safeApprove(address(swapRouter), 0); + tokenSwapIn.safeTransfer(destination, dustIn); } - - uint256 preBal = _preSwap(token, amount); - swapRouter.swapExactTokensForTokens(amount, amountOutMin, path, destination); - _postSwap(token, destination, preBal); } - function _tokensForExactTokensSwap(address token, uint256 amount, bytes memory data) internal { - (uint256 amountOut, address[] memory path, address destination) = - abi.decode(data, (uint256, address[], address)); + function _exactTokensForTokensSwap(address token, address destination, uint256 amount, bytes memory data) + internal + returns (address tokenOut, uint256 amountOut) + { + (address[] memory path) = abi.decode(data, (address[])); - if (path[0] != token) { - revert("Test, not token in path"); - } + path[0] == token; - uint256 preBal = _preSwap(token, amount); - swapRouter.swapExactTokensForTokens(amount, amountOut, path, destination); - _postSwap(token, destination, preBal); - } + tokenOut = path[path.length - 1]; + + IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); + IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + + uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; + uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - function _preSwap(address _tokenIn, uint256 amount) internal returns (uint256 preBal) { - IERC20Upgradeable tokenIn = IERC20Upgradeable(_tokenIn); + tokenSwapIn.safeApprove(address(swapRouter), amount); + swapRouter.swapExactTokensForTokens(amount, 0, path, address(this)); - preBal = tokenIn.balanceOf(address(this)) - amount; + uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; + amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - tokenIn.safeApprove(address(swapRouter), amount); + if (dustIn != 0) { + tokenSwapIn.safeApprove(address(swapRouter), 0); + tokenSwapIn.safeTransfer(destination, dustIn); + } } - function _postSwap(address _tokenIn, address destination, uint256 preBal) internal { - IERC20Upgradeable tokenIn = IERC20Upgradeable(_tokenIn); + function _tokensForExactTokensSwap(address token, address destination, uint256 amount, bytes memory data) + internal + returns (address tokenOut, uint256 amountOut) + { + address[] memory path; + (amountOut, path) = abi.decode(data, (uint256, address[])); + + path[0] == token; + + tokenOut = path[path.length - 1]; + + IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); + IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + + uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; + uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); + + tokenSwapIn.safeApprove(address(swapRouter), amount); + swapRouter.swapTokensForExactTokens(amountOut, amount, path, address(this)); + + uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; + amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - uint256 dust = tokenIn.balanceOf(address(this)) - preBal; - if (dust != 0) { - tokenIn.safeApprove(address(swapRouter), 0); - tokenIn.transfer(destination, dust); + if (dustIn != 0) { + tokenSwapIn.safeApprove(address(swapRouter), 0); + tokenSwapIn.safeTransfer(destination, dustIn); } } diff --git a/AxelarHandler/src/libraries/Path.sol b/AxelarHandler/src/libraries/Path.sol new file mode 100644 index 0000000..af232b3 --- /dev/null +++ b/AxelarHandler/src/libraries/Path.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.6.0; + +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +library BytesLib { + function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) { + require(_length + 31 >= _length, "slice_overflow"); + require(_start + _length >= _start, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { mstore(mc, mload(cc)) } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_start + 20 >= _start, "toAddress_overflow"); + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { + require(_start + 3 >= _start, "toUint24_overflow"); + require(_bytes.length >= _start + 3, "toUint24_outOfBounds"); + uint24 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x3), _start)) + } + + return tempUint; + } +} + +/// @title Functions for manipulating path data for multihop swaps +library Path { + using BytesLib for bytes; + + /// @dev The length of the bytes encoded address + uint256 private constant ADDR_SIZE = 20; + /// @dev The length of the bytes encoded fee + uint256 private constant FEE_SIZE = 3; + + /// @dev The offset of a single token address and pool fee + uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE; + /// @dev The offset of an encoded pool key + uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET; + + /// @notice Returns true iff the path contains two or more pools + /// @param path The encoded swap path + /// @return True if path contains two or more pools, otherwise false + function hasMultiplePools(bytes memory path) internal pure returns (bool) { + return path.length >= MULTIPLE_POOLS_MIN_LENGTH; + } + + /// @notice Returns the number of pools in the path + /// @param path The encoded swap path + /// @return The number of pools in the path + function numPools(bytes memory path) internal pure returns (uint256) { + // Ignore the first token address. From then on every fee and token offset indicates a pool. + return ((path.length - ADDR_SIZE) / NEXT_OFFSET); + } + + /// @notice Decodes the first pool in path + /// @param path The bytes encoded swap path + /// @return tokenA The first token of the given pool + /// @return tokenB The second token of the given pool + /// @return fee The fee level of the pool + function decodeFirstPool(bytes memory path) internal pure returns (address tokenA, address tokenB, uint24 fee) { + tokenA = path.toAddress(0); + fee = path.toUint24(ADDR_SIZE); + tokenB = path.toAddress(NEXT_OFFSET); + } + + /// @notice Decodes the last pool in path + /// @param path The bytes encoded swap path + /// @return tokenA The first token of the given pool + /// @return tokenB The second token of the given pool + /// @return fee The fee level of the pool + function decodeLastPool(bytes memory path) internal pure returns (address tokenA, address tokenB, uint24 fee) {} + + /// @notice Gets the segment corresponding to the first pool in the path + /// @param path The bytes encoded swap path + /// @return The segment containing all data necessary to target the first pool in the path + function getFirstPool(bytes memory path) internal pure returns (bytes memory) { + return path.slice(0, POP_OFFSET); + } + + /// @notice Skips a token + fee element from the buffer and returns the remainder + /// @param path The swap path + /// @return The remaining token + fee elements in the path + function skipToken(bytes memory path) internal pure returns (bytes memory) { + return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); + } +} diff --git a/AxelarHandler/test/AxelarHandler.t.sol b/AxelarHandler/test/AxelarHandler.t.sol index 19662d1..eaf8798 100644 --- a/AxelarHandler/test/AxelarHandler.t.sol +++ b/AxelarHandler/test/AxelarHandler.t.sol @@ -259,7 +259,7 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(token)); - bytes memory payload = abi.encode(uint8(0), abi.encode(true, ALICE)); + bytes memory payload = abi.encode(uint8(0), abi.encode(ALICE)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, 100 ether @@ -286,7 +286,7 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(token)); - bytes memory payload = abi.encode(uint8(0), abi.encode(false, ALICE)); + bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, 100 ether @@ -314,7 +314,7 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(token)); - bytes memory payload = abi.encode(uint8(0), abi.encode(true, ALICE)); + bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, 100 ether @@ -325,16 +325,16 @@ contract AxelarHandlerTest is Test { assertEq(ALICE.balance, 100 ether, "Alice native balance after"); } - function test_executeWithToken_swap() public { - uint256 inputAmount = 5 ether; - uint256 minOutput = 10_000 * 1e6; + function test_executeWithToken_exactInputSingleSwap() public { + uint256 inputAmount = 1 ether; + uint256 amountOutMinimum = 1000 * 1e6; // 1000 USDC string memory symbol = "WETH"; IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(inputToken), "WETH"); IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - vm.label(address(inputToken), "USDC"); + vm.label(address(outputToken), "USDC"); deal(address(inputToken), address(this), inputAmount); inputToken.transfer(address(handler), inputAmount); @@ -349,35 +349,30 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - address[] memory path = new address[](2); - path[0] = address(inputToken); - path[1] = address(outputToken); - bytes memory swapCalldata = - abi.encodeWithSelector(bytes4(0x472b43f3), inputAmount, minOutput, path, address(handler)); - bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE, address(outputToken), swapCalldata)); + bytes memory payload = + abi.encode(uint8(2), abi.encode(address(outputToken), uint24(3000), ALICE, amountOutMinimum, uint160(0))); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount ); - assertEq(inputToken.balanceOf(ALICE), 0, "User got refunded input"); - assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); - assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); - assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); + assertGt(outputToken.balanceOf(ALICE), amountOutMinimum, "Alice output token balance after"); + assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); } - function test_executeWithToken_swap_refundDust() public { - uint256 inputAmount = 4.95 ether; - uint256 dust = 0.05 ether; - uint256 minOutput = 10_000 * 1e6; + function test_executeWithToken_exactInputSwap() public { + uint256 inputAmount = 1 ether; + uint256 amountOutMinimum = 1000 * 1e6; // 1000 USDC string memory symbol = "WETH"; IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(inputToken), "WETH"); IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - vm.label(address(inputToken), "USDC"); + vm.label(address(outputToken), "USDC"); deal(address(inputToken), address(this), inputAmount); inputToken.transfer(address(handler), inputAmount); @@ -392,24 +387,144 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - address[] memory path = new address[](2); - path[0] = address(inputToken); - path[1] = address(outputToken); - bytes memory swapCalldata = - abi.encodeWithSelector(bytes4(0x472b43f3), inputAmount - dust, minOutput, path, address(handler)); - bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE, address(outputToken), swapCalldata)); + bytes memory payload = + abi.encode(uint8(3), abi.encode(address(outputToken), uint24(3000), ALICE, amountOutMinimum, uint160(0))); + + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + ); + + assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); + assertGt(outputToken.balanceOf(ALICE), amountOutMinimum, "Alice output token balance after"); + assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); + } + + function test_executeWithToken_exactOutputSingleSwap() public { + uint256 inputAmount = 1 ether; + uint256 amountOut = 1000 * 1e6; // 1000 USDC + + string memory symbol = "WETH"; + IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + vm.label(address(inputToken), "WETH"); + + IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); + vm.label(address(outputToken), "USDC"); + + deal(address(inputToken), address(this), inputAmount); + inputToken.transfer(address(handler), inputAmount); + + assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); + assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + + deployCodeTo("MockGateway.sol", address(gateway)); + MockGateway mockGateway = MockGateway(address(gateway)); + mockGateway.saveTokenAddress(symbol, address(inputToken)); + + bytes memory payload = + abi.encode(uint8(4), abi.encode(address(outputToken), uint24(3000), ALICE, amountOut, uint160(0))); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount ); - assertEq(inputToken.balanceOf(ALICE), dust, "User didn't got dust refunded"); - assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); - assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); - assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + assertGt(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); + assertEq(outputToken.balanceOf(ALICE), amountOut, "Alice output token balance after"); + assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); } + // function test_executeWithToken_v2_swap() public { + // uint256 inputAmount = 5 ether; + // uint256 minOutput = 10_000 * 1e6; + + // string memory symbol = "WETH"; + // IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + // vm.label(address(inputToken), "WETH"); + + // IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); + // vm.label(address(inputToken), "USDC"); + + // deal(address(inputToken), address(this), inputAmount); + // inputToken.transfer(address(handler), inputAmount); + + // assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); + // assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + // assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); + // assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + + // deployCodeTo("MockGateway.sol", address(gateway)); + // MockGateway mockGateway = MockGateway(address(gateway)); + // mockGateway.saveTokenAddress(symbol, address(inputToken)); + + // address[] memory path = new address[](2); + // path[0] = address(inputToken); + // path[1] = address(outputToken); + + // bytes memory swapCalldata = + // abi.encodeWithSelector(bytes4(0x472b43f3), inputAmount, minOutput, path, address(handler)); + // bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE, address(outputToken), swapCalldata)); + + // handler.executeWithToken( + // keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + // ); + + // assertEq(inputToken.balanceOf(ALICE), 0, "User got refunded input"); + // assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); + // assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); + // assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); + // assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + // } + + // function test_executeWithToken_swap_refundDust() public { + // uint256 inputAmount = 4.95 ether; + // uint256 dust = 0.05 ether; + // uint256 minOutput = 10_000 * 1e6; + + // string memory symbol = "WETH"; + // IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + // vm.label(address(inputToken), "WETH"); + + // IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); + // vm.label(address(inputToken), "USDC"); + + // deal(address(inputToken), address(this), inputAmount); + // inputToken.transfer(address(handler), inputAmount); + + // assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); + // assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + // assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); + // assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + + // deployCodeTo("MockGateway.sol", address(gateway)); + // MockGateway mockGateway = MockGateway(address(gateway)); + // mockGateway.saveTokenAddress(symbol, address(inputToken)); + + // address[] memory path = new address[](2); + // path[0] = address(inputToken); + // path[1] = address(outputToken); + // bytes memory swapCalldata = + // abi.encodeWithSelector(bytes4(0x472b43f3), inputAmount - dust, minOutput, path, address(handler)); + // bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE, address(outputToken), swapCalldata)); + + // handler.executeWithToken( + // keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + // ); + + // assertEq(inputToken.balanceOf(ALICE), dust, "User didn't got dust refunded"); + // assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); + // assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); + // assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); + // assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + // } + function test_swapAndGmpTransferERC20Token_ETH( uint32 domain, address inputToken, From 03fe80ced308a6172e7de4e47491caa6735d5aef Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 2 Aug 2024 23:50:39 +0100 Subject: [PATCH 05/17] Slice and replace bytes path --- AxelarHandler/src/AxelarHandler.sol | 12 ++- AxelarHandler/src/libraries/Path.sol | 98 +++++++++++++++++++++++- AxelarHandler/test/mocks/MockGateway.sol | 27 ++----- 3 files changed, 114 insertions(+), 23 deletions(-) diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index 269808e..ff63732 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -15,7 +15,7 @@ import {UUPSUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/ import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import {ISwapRouter02} from "./interfaces/ISwapRouter02.sol"; -import {Path} from "./libraries/Path.sol"; +import {BytesLib, Path} from "./libraries/Path.sol"; /// @title AxelarHandler /// @notice allows to send and receive tokens to/from other chains through axelar gateway while wrapping the native tokens. @@ -483,7 +483,10 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, (address tokenA,,) = params.path.decodeFirstPool(); - if (tokenA != token) {} + if (tokenA != token) { + bytes memory tokenReplace = BytesLib.toBytes(token); + params.path = BytesLib.concat(tokenReplace, BytesLib.slice(params.path, 20, params.path.length - 20)); + } (, tokenOut,) = params.path.decodeLastPool(); @@ -550,7 +553,10 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, (address tokenA,,) = params.path.decodeFirstPool(); - if (tokenA != token) {} + if (tokenA != token) { + bytes memory tokenReplace = BytesLib.toBytes(token); + params.path = BytesLib.concat(tokenReplace, BytesLib.slice(params.path, 20, params.path.length - 20)); + } (, tokenOut,) = params.path.decodeLastPool(); diff --git a/AxelarHandler/src/libraries/Path.sol b/AxelarHandler/src/libraries/Path.sol index af232b3..38050de 100644 --- a/AxelarHandler/src/libraries/Path.sol +++ b/AxelarHandler/src/libraries/Path.sol @@ -9,6 +9,76 @@ pragma solidity >=0.6.0; * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. */ library BytesLib { + function concat(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bytes memory) { + bytes memory tempBytes; + + assembly { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // Store the length of the first bytes array at the beginning of + // the memory for tempBytes. + let length := mload(_preBytes) + mstore(tempBytes, length) + + // Maintain a memory counter for the current write location in the + // temp bytes array by adding the 32 bytes for the array length to + // the starting location. + let mc := add(tempBytes, 0x20) + // Stop copying when the memory counter reaches the length of the + // first bytes array. + let end := add(mc, length) + + for { + // Initialize a copy counter to the start of the _preBytes data, + // 32 bytes into its memory. + let cc := add(_preBytes, 0x20) + } lt(mc, end) { + // Increase both counters by 32 bytes each iteration. + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // Write the _preBytes data into the tempBytes memory 32 bytes + // at a time. + mstore(mc, mload(cc)) + } + + // Add the length of _postBytes to the current length of tempBytes + // and store it as the new length in the first 32 bytes of the + // tempBytes memory. + length := mload(_postBytes) + mstore(tempBytes, add(length, mload(tempBytes))) + + // Move the memory counter back from a multiple of 0x20 to the + // actual end of the _preBytes data. + mc := end + // Stop copying when the memory counter reaches the new combined + // length of the arrays. + end := add(mc, length) + + for { let cc := add(_postBytes, 0x20) } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { mstore(mc, mload(cc)) } + + // Update the free-memory pointer by padding our last write location + // to 32 bytes: add 31 bytes to the end of tempBytes to move to the + // next 32 byte block, then round down to the nearest multiple of + // 32. If the sum of the length of the two arrays is zero then add + // one before rounding down to leave a blank 32 bytes (the length block with 0). + mstore( + 0x40, + and( + add(add(end, iszero(add(length, mload(_preBytes)))), 31), + not(31) // Round down to the nearest 32 bytes. + ) + ) + } + + return tempBytes; + } + function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) { require(_length + 31 >= _length, "slice_overflow"); require(_start + _length >= _start, "slice_overflow"); @@ -92,6 +162,20 @@ library BytesLib { return tempUint; } + + function toBytes(address _address) public pure returns (bytes memory) { + bytes memory tempBytes; + + assembly { + let m := mload(0x40) + _address := and(_address, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + mstore(add(m, 20), xor(0x140000000000000000000000000000000000000000, a)) + mstore(0x40, add(m, 52)) + tempBytes := m + } + + return tempBytes; + } } /// @title Functions for manipulating path data for multihop swaps @@ -141,7 +225,12 @@ library Path { /// @return tokenA The first token of the given pool /// @return tokenB The second token of the given pool /// @return fee The fee level of the pool - function decodeLastPool(bytes memory path) internal pure returns (address tokenA, address tokenB, uint24 fee) {} + function decodeLastPool(bytes memory path) internal pure returns (address tokenA, address tokenB, uint24 fee) { + path = getLastPool(path); + tokenA = path.toAddress(0); + fee = path.toUint24(ADDR_SIZE); + tokenB = path.toAddress(NEXT_OFFSET); + } /// @notice Gets the segment corresponding to the first pool in the path /// @param path The bytes encoded swap path @@ -150,6 +239,13 @@ library Path { return path.slice(0, POP_OFFSET); } + /// @notice Gets the segment corresponding to the last pool in the path + /// @param path The bytes encoded swap path + /// @return The segment containing all data necessary to target the first pool in the path + function getLastPool(bytes memory path) internal pure returns (bytes memory) { + return path.slice(0, POP_OFFSET); + } + /// @notice Skips a token + fee element from the buffer and returns the remainder /// @param path The swap path /// @return The remaining token + fee elements in the path diff --git a/AxelarHandler/test/mocks/MockGateway.sol b/AxelarHandler/test/mocks/MockGateway.sol index 43c7089..9d67d96 100644 --- a/AxelarHandler/test/mocks/MockGateway.sol +++ b/AxelarHandler/test/mocks/MockGateway.sol @@ -4,35 +4,24 @@ pragma solidity ^0.8.18; contract MockGateway { mapping(bytes32 => address) tokens; - function validateContractCall( - bytes32, - string calldata, - string calldata, - bytes32 - ) external pure returns (bool) { + function validateContractCall(bytes32, string calldata, string calldata, bytes32) external pure returns (bool) { return true; } - function validateContractCallAndMint( - bytes32, - string calldata, - string calldata, - bytes32, - string calldata, - uint256 - ) external pure returns (bool) { + function validateContractCallAndMint(bytes32, string calldata, string calldata, bytes32, string calldata, uint256) + external + pure + returns (bool) + { return true; } - function tokenAddresses(string memory symbol) external returns (address) { + function tokenAddresses(string memory symbol) external view returns (address) { bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); return tokens[symbolHash]; } - function saveTokenAddress( - string memory symbol, - address tokenAddress - ) external { + function saveTokenAddress(string memory symbol, address tokenAddress) external { bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); tokens[symbolHash] = tokenAddress; } From 9c9c0d2d8c370f3d2160d59bbd9cc806ad7378e6 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 2 Aug 2024 23:59:43 +0100 Subject: [PATCH 06/17] Fix toBytes address conversion --- AxelarHandler/src/libraries/Path.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AxelarHandler/src/libraries/Path.sol b/AxelarHandler/src/libraries/Path.sol index 38050de..8bcee37 100644 --- a/AxelarHandler/src/libraries/Path.sol +++ b/AxelarHandler/src/libraries/Path.sol @@ -169,7 +169,7 @@ library BytesLib { assembly { let m := mload(0x40) _address := and(_address, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - mstore(add(m, 20), xor(0x140000000000000000000000000000000000000000, a)) + mstore(add(m, 20), xor(0x140000000000000000000000000000000000000000, _address)) mstore(0x40, add(m, 52)) tempBytes := m } From 44a102343aa1fc9f12d7de69fa0007a6cc69a411 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 3 Aug 2024 00:49:59 +0100 Subject: [PATCH 07/17] Fix getLastPool and Path tests --- AxelarHandler/src/libraries/Path.sol | 4 +-- AxelarHandler/test/Path.t.sol | 46 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 AxelarHandler/test/Path.t.sol diff --git a/AxelarHandler/src/libraries/Path.sol b/AxelarHandler/src/libraries/Path.sol index 8bcee37..6199daf 100644 --- a/AxelarHandler/src/libraries/Path.sol +++ b/AxelarHandler/src/libraries/Path.sol @@ -243,13 +243,13 @@ library Path { /// @param path The bytes encoded swap path /// @return The segment containing all data necessary to target the first pool in the path function getLastPool(bytes memory path) internal pure returns (bytes memory) { - return path.slice(0, POP_OFFSET); + return path.slice(path.length - POP_OFFSET, POP_OFFSET); } /// @notice Skips a token + fee element from the buffer and returns the remainder /// @param path The swap path /// @return The remaining token + fee elements in the path function skipToken(bytes memory path) internal pure returns (bytes memory) { - return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); + return path.slice(path.length, path.length - NEXT_OFFSET); } } diff --git a/AxelarHandler/test/Path.t.sol b/AxelarHandler/test/Path.t.sol new file mode 100644 index 0000000..247ab6c --- /dev/null +++ b/AxelarHandler/test/Path.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; + +import {BytesLib, Path} from "src/libraries/Path.sol"; + +contract PathTest is Test { + using Path for bytes; + + function test_toBytes() public { + address testAddr = makeAddr("test"); + bytes memory testAddrBytes = BytesLib.toBytes(testAddr); + + assertEq(testAddr, BytesLib.toAddress(testAddrBytes, 0), "Address to bytes lib"); + assertEq(testAddr, address(uint160(bytes20(testAddrBytes))), "Address to bytes typecast"); + } + + function test_decodeFirstPool() public { + address token1 = makeAddr("TOKEN 1"); + address token2 = makeAddr("TOKEN 2"); + address token3 = makeAddr("TOKEN 3"); + + bytes memory testPath = abi.encodePacked(token1, uint24(100), token2, uint24(3000), token3); + + (address tokenA, address tokenB, uint24 fee) = testPath.decodeFirstPool(); + + assertEq(tokenA, token1, "Token A not properly decoded"); + assertEq(tokenB, token2, "Token B not properly decoded"); + assertEq(uint24(100), fee, "fee not properly decoded"); + } + + function test_decodeLastPool() public { + address token1 = makeAddr("TOKEN 1"); + address token2 = makeAddr("TOKEN 2"); + address token3 = makeAddr("TOKEN 3"); + + bytes memory testPath = abi.encodePacked(token1, uint24(100), token2, uint24(3000), token3); + + (address tokenA, address tokenB, uint24 fee) = testPath.decodeLastPool(); + + assertEq(tokenA, token2, "Token A not properly decoded"); + assertEq(tokenB, token3, "Token B not properly decoded"); + assertEq(uint24(3000), fee, "fee not properly decoded"); + } +} From 502ef75a855cbf2c62acd3127614ea9689d8c50b Mon Sep 17 00:00:00 2001 From: Manu Date: Wed, 7 Aug 2024 19:24:54 +0200 Subject: [PATCH 08/17] Bugfixes and tests --- AxelarHandler/.env.example | 2 +- AxelarHandler/foundry.toml | 2 + AxelarHandler/src/AxelarHandler.sol | 10 +- AxelarHandler/test/AxelarHandler.t.sol | 297 ++++++++++++++++++------ AxelarHandler/test/Environment.sol | 106 +++------ AxelarHandler/test/Path.t.sol | 56 +++++ AxelarHandler/test/mocks/MockRouter.sol | 98 ++++++++ 7 files changed, 425 insertions(+), 146 deletions(-) create mode 100644 AxelarHandler/test/mocks/MockRouter.sol diff --git a/AxelarHandler/.env.example b/AxelarHandler/.env.example index 46fd429..45f0a2b 100644 --- a/AxelarHandler/.env.example +++ b/AxelarHandler/.env.example @@ -1,2 +1,2 @@ export PRIVATE_KEY= -export RPC_URL= \ No newline at end of file +export RPC_MAINNET= \ No newline at end of file diff --git a/AxelarHandler/foundry.toml b/AxelarHandler/foundry.toml index e883058..3dad954 100644 --- a/AxelarHandler/foundry.toml +++ b/AxelarHandler/foundry.toml @@ -4,3 +4,5 @@ out = "out" libs = ["lib"] # See more config options https://github.com/foundry-rs/foundry/tree/master/config +[rpc_endpoints] +mainnet = "${RPC_MAINNET}" \ No newline at end of file diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index ff63732..bc696a6 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -476,7 +476,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, returns (address tokenOut, uint256 amountOut) { ISwapRouter02.ExactInputParams memory params; - (params.path) = abi.decode(data, (bytes)); + params.path = data; params.recipient = address(this); params.amountIn = amount; @@ -551,14 +551,14 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, params.amountOut = amountOut; params.amountInMaximum = amount; - (address tokenA,,) = params.path.decodeFirstPool(); + (, address tokenB,) = params.path.decodeLastPool(); - if (tokenA != token) { + if (tokenB != token) { bytes memory tokenReplace = BytesLib.toBytes(token); - params.path = BytesLib.concat(tokenReplace, BytesLib.slice(params.path, 20, params.path.length - 20)); + params.path = BytesLib.concat(BytesLib.slice(params.path, 0, params.path.length - 20), tokenReplace); } - (, tokenOut,) = params.path.decodeLastPool(); + (tokenOut,,) = params.path.decodeFirstPool(); IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); diff --git a/AxelarHandler/test/AxelarHandler.t.sol b/AxelarHandler/test/AxelarHandler.t.sol index eaf8798..d825342 100644 --- a/AxelarHandler/test/AxelarHandler.t.sol +++ b/AxelarHandler/test/AxelarHandler.t.sol @@ -3,38 +3,46 @@ pragma solidity ^0.8.18; import "forge-std/Test.sol"; import "./Environment.sol"; -import "./mocks/MockGateway.sol"; +import {MockGateway} from "./mocks/MockGateway.sol"; +import {MockRouter} from "./mocks/MockRouter.sol"; import {AxelarHandler} from "src/AxelarHandler.sol"; import {IAxelarExecutable} from "lib/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarExecutable.sol"; import {IAxelarGateway} from "lib/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {IERC20Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; import {ERC1967Proxy} from "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {ISwapRouter02} from "src/interfaces/ISwapRouter02.sol"; + contract AxelarHandlerTest is Test { + string public constant FORK_CHAIN = "mainnet"; + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address public immutable ALICE = makeAddr("ALICE"); address public immutable BOB = makeAddr("BOB"); + bool public isForked; + AxelarHandler public handler; IAxelarGateway public gateway; - Environment public env; - address public router; + ISwapRouter02 public router; - function setUp() public { - env = new Environment(); + modifier forked() { + isForked = true; + Environment env = new Environment(); env.setEnv(1); vm.makePersistent(address(env)); + vm.createSelectFork(vm.rpcUrl(FORK_CHAIN)); - vm.createSelectFork("https://eth.llamarpc.com"); + gateway = IAxelarGateway(env.gateway()); + address gasService = env.gasService(); + router = ISwapRouter02(env.swapRouter()); - gateway = IAxelarGateway(0x4F4495243837681061C4743b74B3eEdf548D56A5); - address gasService = 0x2d5d7d31F671F86C782533cc367F14109a082712; - router = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; string memory wethSymbol = "WETH"; AxelarHandler handlerImpl = new AxelarHandler(); @@ -44,15 +52,43 @@ contract AxelarHandlerTest is Test { ); handler = AxelarHandler(payable(address(handlerProxy))); - handler.setSwapRouter(router); + handler.setSwapRouter(address(router)); vm.label(address(handler), "HANDLER"); vm.label(address(gateway), "GATEWAY"); vm.label(gasService, "GAS SERVICE"); vm.label(gateway.tokenAddresses(wethSymbol), "WETH"); + + _; + + isForked = false; } - function test_sendNativeToken() public { + modifier local() { + gateway = IAxelarGateway(address(new MockGateway())); + address gasService = makeAddr("GAS SERVICE"); + + router = ISwapRouter02(address(new MockRouter())); + + string memory wethSymbol = "WETH"; + + AxelarHandler handlerImpl = new AxelarHandler(); + ERC1967Proxy handlerProxy = new ERC1967Proxy( + address(handlerImpl), + abi.encodeWithSignature("initialize(address,address,string)", address(gateway), gasService, wethSymbol) + ); + handler = AxelarHandler(payable(address(handlerProxy))); + + handler.setSwapRouter(address(router)); + + vm.label(address(handler), "HANDLER"); + vm.label(address(gateway), "GATEWAY"); + vm.label(gateway.tokenAddresses(wethSymbol), "WETH"); + + _; + } + + function test_sendNativeToken() public forked { vm.deal(ALICE, 10 ether); assertEq(ALICE.balance, 10 ether, "Native balance before sending."); @@ -64,7 +100,7 @@ contract AxelarHandlerTest is Test { assertEq(address(handler).balance, 0, "Ether left in the contract."); } - function test_sendNativeToken_NoAmount() public { + function test_sendNativeToken_NoAmount() public forked { vm.deal(ALICE, 10 ether); assertEq(ALICE.balance, 10 ether, "Native balance before sending."); @@ -74,7 +110,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_sendERC20Token() public { + function test_sendERC20Token() public forked { string memory symbol = "WETH"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); @@ -90,7 +126,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(address(handler)), 0, "Tokens left in the contract."); } - function test_sendERC20Token_WrongSymbol() public { + function test_sendERC20Token_WrongSymbol() public forked { string memory symbol = "WBTCx"; vm.startPrank(ALICE); vm.expectRevert(AxelarHandler.TokenNotSupported.selector); @@ -98,7 +134,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_sendERC20Token_NoAllowance() public { + function test_sendERC20Token_NoAllowance() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); @@ -113,7 +149,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferNativeToken() public { + function test_gmpTransferNativeToken() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -127,7 +163,7 @@ contract AxelarHandlerTest is Test { assertEq(address(handler).balance, 0, "Ether left in the contract."); } - function test_gmpTransferNativeToken_ZeroGas() public { + function test_gmpTransferNativeToken_ZeroGas() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -139,7 +175,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferNativeToken_ZeroAmount() public { + function test_gmpTransferNativeToken_ZeroAmount() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -151,7 +187,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferNativeToken_AmountMismatch() public { + function test_gmpTransferNativeToken_AmountMismatch() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -163,7 +199,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferERC20Token() public { + function test_gmpTransferERC20Token() public forked { vm.deal(ALICE, 25 ether); assertEq(ALICE.balance, 25 ether, "Native balance before sending."); @@ -186,7 +222,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(address(handler)), 0, "Tokens left in the contract."); } - function test_gmpTransferERC20Token_GasMismatch() public { + function test_gmpTransferERC20Token_GasMismatch() public forked { vm.deal(ALICE, 0.5 ether); assertEq(ALICE.balance, 0.5 ether, "Native balance before sending."); @@ -205,7 +241,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferERC20Token_ZeroGas() public { + function test_gmpTransferERC20Token_ZeroGas() public forked { vm.deal(ALICE, 0.5 ether); assertEq(ALICE.balance, 0.5 ether, "Native balance before sending."); @@ -224,7 +260,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferERC20TokenGasTokenPayment() public { + function test_gmpTransferERC20TokenGasTokenPayment() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WBTC"); @@ -243,7 +279,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(address(handler)), 0, "Tokens left in the contract."); } - function test_executeWithToken_nonunwrap_nonWETH() public { + function test_executeWithToken_nonunwrap_nonWETH() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WBTC"); @@ -270,7 +306,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(ALICE), 100 ether, "Alice balance after"); } - function test_executeWithToken_unwrap_nonWETH() public { + function test_executeWithToken_unwrap_nonWETH() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WBTC"); @@ -297,7 +333,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(ALICE), 100 ether, "Alice balance after"); } - function test_executeWithToken_unwrap_WETH() public { + function test_executeWithToken_unwrap_WETH() public forked { string memory symbol = "WETH"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WETH"); @@ -325,7 +361,75 @@ contract AxelarHandlerTest is Test { assertEq(ALICE.balance, 100 ether, "Alice native balance after"); } - function test_executeWithToken_exactInputSingleSwap() public { + function test_executeWithToken_exactInputSingleSwap_Fork() public forked { + string memory symbol = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbol); + vm.label(address(tokenIn), symbol); + + address tokenOut = USDC; + vm.label(address(tokenOut), "USDC"); + + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOutMin = 1_000 * 1e6; // 1,000 USDC + address destination = ALICE; + bool unwrap = false; + + _execute_exactInputSingleSwap(symbol, tokenIn, tokenOut, amountIn, amountOutMin, destination, unwrap); + } + + function _execute_exactInputSingleSwap( + string memory symbol, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMin, + address destination, + bool unwrap + ) internal { + _mockGateway(symbol, tokenIn, amountIn); + + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); + + assertEq(inputToken.balanceOf(address(handler)), amountIn, "Handler input token balance before"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + assertEq(inputToken.balanceOf(destination), 0, "Destination input token balance before"); + assertEq(outputToken.balanceOf(destination), 0, "Destination output token balance before"); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = abi.encode(uint8(0), abi.encode(tokenOut, uint24(3000), uint160(0))); + + bytes memory payload = abi.encode(uint8(2), abi.encode(destination, amountOutMin, unwrap, swaps)); + + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn + ); + + assertEq(inputToken.balanceOf(destination), 0, "Destination input token balance after"); + if (unwrap) { + assertGt(destination.balance, amountOutMin, "Destination output native balance after"); + assertEq(address(handler).balance, 0, "Handler native token balance after"); + } else { + assertGt(IERC20(tokenOut).balanceOf(destination), amountOutMin, "Destination output token balance after"); + } + assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); + } + + function _mockGateway(string memory symbol, address tokenIn, uint256 amountIn) internal { + deal(tokenIn, address(handler), amountIn); + + if (isForked) { + deployCodeTo("MockGateway.sol", address(gateway)); + } + + MockGateway mockGateway = MockGateway(address(gateway)); + mockGateway.saveTokenAddress(symbol, tokenIn); + } + + function test_executeWithToken_exactInputSwap() public forked { uint256 inputAmount = 1 ether; uint256 amountOutMinimum = 1000 * 1e6; // 1000 USDC @@ -349,8 +453,9 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - bytes memory payload = - abi.encode(uint8(2), abi.encode(address(outputToken), uint24(3000), ALICE, amountOutMinimum, uint160(0))); + bytes[] memory swaps = new bytes[](1); + swaps[0] = abi.encode(uint8(1), abi.encodePacked(address(inputToken), uint24(500), address(outputToken))); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOutMinimum, false, swaps)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount @@ -363,9 +468,52 @@ contract AxelarHandlerTest is Test { assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); } - function test_executeWithToken_exactInputSwap() public { + function test_executeWithToken_exactTokensForTokensSwap() public forked { + uint256 inputAmount = 5 ether; + uint256 minOutput = 10_000 * 1e6; + + string memory symbol = "WETH"; + IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + vm.label(address(inputToken), "WETH"); + + IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); + vm.label(address(inputToken), "USDC"); + + deal(address(inputToken), address(this), inputAmount); + inputToken.transfer(address(handler), inputAmount); + + assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); + assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + + deployCodeTo("MockGateway.sol", address(gateway)); + MockGateway mockGateway = MockGateway(address(gateway)); + mockGateway.saveTokenAddress(symbol, address(inputToken)); + + address[] memory path = new address[](2); + path[0] = address(inputToken); + path[1] = address(outputToken); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = abi.encode(uint8(2), abi.encode(path)); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, minOutput, false, swaps)); + + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + ); + + assertEq(inputToken.balanceOf(ALICE), 0, "User got refunded input"); + assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); + assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); + assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + } + + function test_executeWithToken_exactOutputSingleSwap() public forked { uint256 inputAmount = 1 ether; - uint256 amountOutMinimum = 1000 * 1e6; // 1000 USDC + uint256 amountOut = 1000 * 1e6; // 1000 USDC string memory symbol = "WETH"; IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); @@ -387,21 +535,22 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - bytes memory payload = - abi.encode(uint8(3), abi.encode(address(outputToken), uint24(3000), ALICE, amountOutMinimum, uint160(0))); + bytes[] memory swaps = new bytes[](1); + swaps[0] = abi.encode(uint8(3), abi.encode(address(outputToken), uint256(amountOut), uint24(3000), uint160(0))); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOut, false, swaps)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount ); - assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); - assertGt(outputToken.balanceOf(ALICE), amountOutMinimum, "Alice output token balance after"); + assertGt(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); + assertEq(outputToken.balanceOf(ALICE), amountOut, "Alice output token balance after"); assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); } - function test_executeWithToken_exactOutputSingleSwap() public { + function test_executeWithToken_exactOutputSwap() public forked { uint256 inputAmount = 1 ether; uint256 amountOut = 1000 * 1e6; // 1000 USDC @@ -425,8 +574,12 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - bytes memory payload = - abi.encode(uint8(4), abi.encode(address(outputToken), uint24(3000), ALICE, amountOut, uint160(0))); + bytes[] memory swaps = new bytes[](1); + swaps[0] = abi.encode( + uint8(4), + abi.encode(uint256(amountOut), abi.encodePacked(address(outputToken), uint24(500), address(inputToken))) + ); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOut, false, swaps)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount @@ -439,48 +592,48 @@ contract AxelarHandlerTest is Test { assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); } - // function test_executeWithToken_v2_swap() public { - // uint256 inputAmount = 5 ether; - // uint256 minOutput = 10_000 * 1e6; + function test_executeWithToken_tokensForExactTokensSwap() public forked { + uint256 inputAmount = 5 ether; + uint256 amountOut = 10_000 * 1e6; - // string memory symbol = "WETH"; - // IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); - // vm.label(address(inputToken), "WETH"); + string memory symbol = "WETH"; + IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + vm.label(address(inputToken), "WETH"); - // IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - // vm.label(address(inputToken), "USDC"); + IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); + vm.label(address(inputToken), "USDC"); - // deal(address(inputToken), address(this), inputAmount); - // inputToken.transfer(address(handler), inputAmount); + deal(address(inputToken), address(this), inputAmount); + inputToken.transfer(address(handler), inputAmount); - // assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); - // assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); - // assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); - // assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); + assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); - // deployCodeTo("MockGateway.sol", address(gateway)); - // MockGateway mockGateway = MockGateway(address(gateway)); - // mockGateway.saveTokenAddress(symbol, address(inputToken)); + deployCodeTo("MockGateway.sol", address(gateway)); + MockGateway mockGateway = MockGateway(address(gateway)); + mockGateway.saveTokenAddress(symbol, address(inputToken)); - // address[] memory path = new address[](2); - // path[0] = address(inputToken); - // path[1] = address(outputToken); + address[] memory path = new address[](2); + path[0] = address(inputToken); + path[1] = address(outputToken); - // bytes memory swapCalldata = - // abi.encodeWithSelector(bytes4(0x472b43f3), inputAmount, minOutput, path, address(handler)); - // bytes memory payload = abi.encode(uint8(1), abi.encode(ALICE, address(outputToken), swapCalldata)); + bytes[] memory swaps = new bytes[](1); + swaps[0] = abi.encode(uint8(5), abi.encode(amountOut, path)); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOut, false, swaps)); - // handler.executeWithToken( - // keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount - // ); + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + ); - // assertEq(inputToken.balanceOf(ALICE), 0, "User got refunded input"); - // assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); - // assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); - // assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); - // assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); - // } + assertGt(inputToken.balanceOf(ALICE), 0, "User got refunded input"); + assertEq(outputToken.balanceOf(ALICE), amountOut, "User balance didn't increase"); + assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); + assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); + assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + } // function test_executeWithToken_swap_refundDust() public { // uint256 inputAmount = 4.95 ether; @@ -530,7 +683,7 @@ contract AxelarHandlerTest is Test { address inputToken, uint256 inputAmount, bytes memory swapCalldata - ) public { + ) public forked { uint256 amount = 2 ether; uint256 gasAmount = 0.5 ether; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses("WBTC")); @@ -571,7 +724,7 @@ contract AxelarHandlerTest is Test { address inputToken, uint256 inputAmount, bytes memory swapCalldata - ) public { + ) public forked { uint256 amount = 2 ether; uint256 gasAmount = 0.5 ether; IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses("WETH")); diff --git a/AxelarHandler/test/Environment.sol b/AxelarHandler/test/Environment.sol index dc150e3..89966ea 100644 --- a/AxelarHandler/test/Environment.sol +++ b/AxelarHandler/test/Environment.sol @@ -4,75 +4,43 @@ pragma solidity ^0.8.18; import "forge-std/Test.sol"; contract Environment is Test { - address public constant AXELAR_GATEWAY_MAINNET = - 0x4F4495243837681061C4743b74B3eEdf548D56A5; - address public constant AXELAR_GATEWAY_BNB = - 0x304acf330bbE08d1e512eefaa92F6a57871fD895; - address public constant AXELAR_GATEWAY_POLYGON = - 0x6f015F16De9fC8791b234eF68D486d2bF203FBA8; - address public constant AXELAR_GATEWAY_AVALANCHE = - 0x5029C0EFf6C34351a0CEc334542cDb22c7928f78; - address public constant AXELAR_GATEWAY_ARBITRUM = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_OPTIMISM = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_BASE = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_LINEA = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_MANTLE = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_FANTOM = - 0x304acf330bbE08d1e512eefaa92F6a57871fD895; - address public constant AXELAR_GATEWAY_MOONBEAM = - 0x4F4495243837681061C4743b74B3eEdf548D56A5; - address public constant AXELAR_GATEWAY_CELO = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_FILECOIN = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_KAVA = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_BLAST = - 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GATEWAY_POLYGON_MUMBAI = - 0xBF62ef1486468a6bd26Dd669C06db43dEd5B849B; - address public constant AXELAR_GATEWAY_ETHEREUM_GOERLI = - 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_MAINNET = 0x4F4495243837681061C4743b74B3eEdf548D56A5; + address public constant AXELAR_GATEWAY_BNB = 0x304acf330bbE08d1e512eefaa92F6a57871fD895; + address public constant AXELAR_GATEWAY_POLYGON = 0x6f015F16De9fC8791b234eF68D486d2bF203FBA8; + address public constant AXELAR_GATEWAY_AVALANCHE = 0x5029C0EFf6C34351a0CEc334542cDb22c7928f78; + address public constant AXELAR_GATEWAY_ARBITRUM = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_OPTIMISM = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_BASE = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_LINEA = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_MANTLE = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_FANTOM = 0x304acf330bbE08d1e512eefaa92F6a57871fD895; + address public constant AXELAR_GATEWAY_MOONBEAM = 0x4F4495243837681061C4743b74B3eEdf548D56A5; + address public constant AXELAR_GATEWAY_CELO = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_FILECOIN = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_KAVA = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_BLAST = 0xe432150cce91c13a887f7D836923d5597adD8E31; + address public constant AXELAR_GATEWAY_POLYGON_MUMBAI = 0xBF62ef1486468a6bd26Dd669C06db43dEd5B849B; + address public constant AXELAR_GATEWAY_ETHEREUM_GOERLI = 0xe432150cce91c13a887f7D836923d5597adD8E31; - address public constant AXELAR_GAS_SERVICE_MAINNET = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_BNB = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_POLYGON = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_AVALANCHE = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_ARBITRUM = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_OPTIMISM = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_BASE = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_LINEA = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_MANTLE = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_FANTOM = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_MOONBEAM = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_CELO = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_FILECOIN = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_KAVA = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_BLAST = - 0x2d5d7d31F671F86C782533cc367F14109a082712; - address public constant AXELAR_GAS_SERVICE_POLYGON_MUMBAI = - 0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6; - address public constant AXELAR_GAS_SERVICE_ETHEREUM_GOERLI = - 0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6; + address public constant AXELAR_GAS_SERVICE_MAINNET = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_BNB = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_POLYGON = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_AVALANCHE = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_ARBITRUM = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_OPTIMISM = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_BASE = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_LINEA = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_MANTLE = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_FANTOM = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_MOONBEAM = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_CELO = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_FILECOIN = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_KAVA = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_BLAST = 0x2d5d7d31F671F86C782533cc367F14109a082712; + address public constant AXELAR_GAS_SERVICE_POLYGON_MUMBAI = 0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6; + address public constant AXELAR_GAS_SERVICE_ETHEREUM_GOERLI = 0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6; + + address public constant UNISWAP_SWAP_ROUTER_02_MAINNET = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; string public constant AXELAR_WETH_SYMBOL_MAINNET = "WETH"; string public constant AXELAR_WETH_SYMBOL_BNB = "WBNB"; @@ -94,12 +62,14 @@ contract Environment is Test { address public gateway; address public gasService; + address public swapRouter; string public wethSymbol; function setEnv(uint256 chainId) public { if (chainId == 1) { gateway = AXELAR_GATEWAY_MAINNET; gasService = AXELAR_GAS_SERVICE_MAINNET; + swapRouter = UNISWAP_SWAP_ROUTER_02_MAINNET; wethSymbol = AXELAR_WETH_SYMBOL_MAINNET; } else if (chainId == 56) { gateway = AXELAR_GATEWAY_BNB; diff --git a/AxelarHandler/test/Path.t.sol b/AxelarHandler/test/Path.t.sol index 247ab6c..a51a5b9 100644 --- a/AxelarHandler/test/Path.t.sol +++ b/AxelarHandler/test/Path.t.sol @@ -30,6 +30,26 @@ contract PathTest is Test { assertEq(uint24(100), fee, "fee not properly decoded"); } + function test_fuzz_decodeLastPool(uint256 poolNum) public { + poolNum = bound(poolNum, 1, 20); + + address token = makeAddr("TOKEN 0"); + address newToken; + bytes memory path; + for (uint256 i = 1; i <= poolNum; ++i) { + token = newToken; + newToken = makeAddr(string.concat("TOKEN ", vm.toString(i))); + bytes memory newPath = abi.encodePacked(token, uint24(100), newToken); + path = BytesLib.concat(path, newPath); + } + + (address tokenA, address tokenB, uint24 fee) = path.decodeLastPool(); + + assertEq(tokenA, token, "Token A not properly decoded"); + assertEq(tokenB, newToken, "Token B not properly decoded"); + assertEq(uint24(100), fee, "fee not properly decoded"); + } + function test_decodeLastPool() public { address token1 = makeAddr("TOKEN 1"); address token2 = makeAddr("TOKEN 2"); @@ -43,4 +63,40 @@ contract PathTest is Test { assertEq(tokenB, token3, "Token B not properly decoded"); assertEq(uint24(3000), fee, "fee not properly decoded"); } + + function test_replaceFirstToken() public { + address token1 = makeAddr("TOKEN 1"); + address token2 = makeAddr("TOKEN 2"); + address token3 = makeAddr("TOKEN 3"); + address replacementToken = makeAddr("TOKEN REPLACED"); + + bytes memory testPath = abi.encodePacked(token1, uint24(100), token2, uint24(3000), token3); + + bytes memory replacementBytes = BytesLib.toBytes(replacementToken); + testPath = BytesLib.concat(replacementBytes, BytesLib.slice(testPath, 20, testPath.length - 20)); + + (address tokenA, address tokenB, uint24 fee) = testPath.decodeFirstPool(); + + assertEq(tokenA, replacementToken, "Decode Replaced Token"); + assertEq(tokenB, token2, "Decode Token B"); + assertEq(fee, uint24(100), "Decode Fee"); + } + + function test_replaceLastToken() public { + address token1 = makeAddr("TOKEN 1"); + address token2 = makeAddr("TOKEN 2"); + address token3 = makeAddr("TOKEN 3"); + address replacementToken = makeAddr("TOKEN REPLACED"); + + bytes memory testPath = abi.encodePacked(token1, uint24(100), token2, uint24(3000), token3); + + bytes memory replacementBytes = BytesLib.toBytes(replacementToken); + testPath = BytesLib.concat(BytesLib.slice(testPath, 0, testPath.length - 20), replacementBytes); + + (address tokenA, address tokenB, uint24 fee) = testPath.decodeLastPool(); + + assertEq(tokenA, token2, "Decode Token A"); + assertEq(tokenB, replacementToken, "Decode Replaced Token"); + assertEq(fee, uint24(3000), "Decode Fee"); + } } diff --git a/AxelarHandler/test/mocks/MockRouter.sol b/AxelarHandler/test/mocks/MockRouter.sol new file mode 100644 index 0000000..d5482f8 --- /dev/null +++ b/AxelarHandler/test/mocks/MockRouter.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {ISwapRouter02} from "src/interfaces/ISwapRouter02.sol"; +import {BytesLib, Path} from "src/libraries/Path.sol"; + +contract MockRouter is Test, ISwapRouter02 { + uint256 nextRate; + + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut) { + IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + + uint256 calcOutput = (params.amountIn * nextRate) / 10_000; + amountOut = calcOutput >= params.amountOutMinimum ? calcOutput : params.amountOutMinimum; + + deal(params.tokenOut, address(this), amountOut); + + IERC20(params.tokenOut).transfer(params.recipient, amountOut); + } + + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut) { + (address tokenIn,,) = Path.decodeFirstPool(params.path); + (, address tokenOut,) = Path.decodeLastPool(params.path); + IERC20(tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + + uint256 calcOutput = (params.amountIn * nextRate) / 10_000; + amountOut = calcOutput >= params.amountOutMinimum ? calcOutput : params.amountOutMinimum; + + deal(tokenOut, address(this), amountOut); + + IERC20(tokenOut).transfer(params.recipient, amountOut); + } + + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn) { + uint256 calcInput = (params.amountOut * nextRate) / 10_000; + amountIn = calcInput >= params.amountInMaximum ? params.amountInMaximum : calcInput; + + IERC20(params.tokenIn).transferFrom(msg.sender, address(this), amountIn); + + deal(params.tokenOut, address(this), params.amountOut); + IERC20(params.tokenOut).transfer(params.recipient, params.amountOut); + } + + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn) { + uint256 calcInput = (params.amountOut * nextRate) / 10_000; + amountIn = calcInput >= params.amountInMaximum ? params.amountInMaximum : calcInput; + + (, address tokenIn,) = Path.decodeLastPool(params.path); + (address tokenOut,,) = Path.decodeFirstPool(params.path); + IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); + + deal(tokenOut, address(this), params.amountOut); + IERC20(tokenOut).transfer(params.recipient, params.amountOut); + } + + function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to) + external + payable + returns (uint256 amountIn) + { + uint256 calcInput = (amountOut * nextRate) / 10_000; + amountIn = calcInput >= amountInMax ? amountInMax : calcInput; + + IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn); + + address tokenOut = path[path.length - 1]; + deal(tokenOut, address(this), amountOut); + IERC20(tokenOut).transfer(to, amountOut); + } + + function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to) + external + payable + returns (uint256 amountOut) + { + IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn); + + uint256 calcOutput = (amountIn * nextRate) / 10_000; + amountOut = calcOutput >= amountOutMin ? calcOutput : amountOutMin; + + address tokenOut = path[path.length - 1]; + deal(tokenOut, address(this), amountOut); + IERC20(tokenOut).transfer(to, amountOut); + } + + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results) {} + + function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory results) {} + + function multicall(bytes32 previousBlockhash, bytes[] calldata data) + external + payable + returns (bytes[] memory results) + {} +} From f8f638f5bde9a3586404bce45067bb7bdf51db26 Mon Sep 17 00:00:00 2001 From: Manu Date: Wed, 7 Aug 2024 21:13:54 +0200 Subject: [PATCH 09/17] Simulate execute with token script --- .../script/SimulateExecuteWithToken.s.sol | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 AxelarHandler/script/SimulateExecuteWithToken.s.sol diff --git a/AxelarHandler/script/SimulateExecuteWithToken.s.sol b/AxelarHandler/script/SimulateExecuteWithToken.s.sol new file mode 100644 index 0000000..988638d --- /dev/null +++ b/AxelarHandler/script/SimulateExecuteWithToken.s.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; +import {Environment} from "test/Environment.sol"; +import {MockGateway} from "test/mocks/MockGateway.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {ERC1967Proxy} from "lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {AxelarHandler} from "src/AxelarHandler.sol"; + +contract SimulateExecuteWithToken is Script, Test { + AxelarHandler public handler; + Environment public env; + MockGateway public mockGateway; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl("mainnet")); + + env = new Environment(); + env.setEnv(block.chainid); + + mockGateway = new MockGateway(); + + address gateway = address(mockGateway); + address gasService = env.gasService(); + address swapRouter = env.swapRouter(); + string memory wethSymbol = env.wethSymbol(); + + AxelarHandler handlerImpl = new AxelarHandler(); + ERC1967Proxy handlerProxy = new ERC1967Proxy( + address(handlerImpl), + abi.encodeWithSignature("initialize(address,address,string)", gateway, gasService, wethSymbol) + ); + handler = AxelarHandler(payable(address(handlerProxy))); + + handler.setSwapRouter(swapRouter); + } + + function run() public { + string memory tokenInputSymbol = "WETH"; + address tokenIn = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address tokenOut = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; //USDC + address destination = 0x0000000000000000000000000000000000000048; + uint256 amountIn = 1 ether; + uint256 amountOutMin = 1_000 * 1e6; + bool unwrap = false; + + mockGateway.saveTokenAddress(tokenInputSymbol, tokenIn); + deal(address(tokenIn), address(handler), amountIn); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = abi.encode(uint8(0), abi.encode(tokenOut, uint24(3000), uint160(0))); + bytes memory examplePayload = abi.encode(uint8(2), abi.encode(destination, amountOutMin, unwrap, swaps)); + + console2.logBytes(examplePayload); + + console2.log("Token Out Destination Balance Before: %s", IERC20(tokenOut).balanceOf(destination)); + + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), + "osmosis-7", + "mock_address", + examplePayload, + tokenInputSymbol, + amountIn + ); + + console2.log("Token Out Destination Balance After: %s", IERC20(tokenOut).balanceOf(destination)); + } + + function run( + bytes memory payload, + string memory tokenSymbol, + address token, + address tokenOut, + address destination, + uint256 amount + ) public { + mockGateway.saveTokenAddress(tokenSymbol, token); + deal(address(token), address(handler), amount); + + console2.log("Token Out Destination Balance Before: %s", IERC20(tokenOut).balanceOf(destination)); + + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, tokenSymbol, amount + ); + + console2.log("Token Out Destination Balance After: %s", IERC20(tokenOut).balanceOf(destination)); + } +} From 3f54c9212769e7cd3aded0eb27857347026ffd9a Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 8 Aug 2024 17:44:43 +0200 Subject: [PATCH 10/17] Fix to use SafeERC20 --- AxelarHandler/src/AxelarHandler.sol | 51 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index bc696a6..59fd678 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -6,9 +6,8 @@ import {IWETH} from "./interfaces/IWETH.sol"; import {IAxelarGasService} from "lib/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol"; import {AxelarExecutableUpgradeable} from "./AxelarExecutableUpgradeable.sol"; -import {IERC20Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable} from - "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {Ownable2StepUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; import {UUPSUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; @@ -21,7 +20,7 @@ import {BytesLib, Path} from "./libraries/Path.sol"; /// @notice allows to send and receive tokens to/from other chains through axelar gateway while wrapping the native tokens. /// @author Skip Protocol. contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { - using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeERC20 for IERC20; using Path for bytes; error EmptySymbol(); @@ -128,7 +127,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, if (amount == 0) revert ZeroAmount(); if (bytes(symbol).length == 0) revert EmptySymbol(); - IERC20Upgradeable token = IERC20Upgradeable(_getTokenAddress(symbol)); + IERC20 token = IERC20(_getTokenAddress(symbol)); token.safeTransferFrom(msg.sender, address(this), amount); @@ -190,7 +189,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, if (bytes(symbol).length == 0) revert EmptySymbol(); // Get the token address. - IERC20Upgradeable token = IERC20Upgradeable(_getTokenAddress(symbol)); + IERC20 token = IERC20(_getTokenAddress(symbol)); // Transfer the amount from the msg.sender. token.safeTransferFrom(msg.sender, address(this), amount); @@ -228,7 +227,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, if (bytes(symbol).length == 0) revert EmptySymbol(); // Get the address of the output token based on the symbol provided - IERC20Upgradeable outputToken = IERC20Upgradeable(_getTokenAddress(symbol)); + IERC20 outputToken = IERC20(_getTokenAddress(symbol)); uint256 outputAmount; if (inputToken == address(0)) { @@ -262,7 +261,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, if (gasPaymentAmount != msg.value) revert(); // Transfer input ERC20 tokens to the contract - IERC20Upgradeable token = IERC20Upgradeable(inputToken); + IERC20 token = IERC20(inputToken); token.safeTransferFrom(msg.sender, address(this), amount); // Approve the swap router to spend the input tokens @@ -286,7 +285,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, // Refund the remaining amount if (dust != 0) { - token.transfer(msg.sender, dust); + token.safeTransfer(msg.sender, dust); // Revoke approval token.safeApprove(address(swapRouter), 0); @@ -323,7 +322,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, if (bytes(symbol).length == 0) revert EmptySymbol(); // Get the token address. - IERC20Upgradeable token = IERC20Upgradeable(_getTokenAddress(symbol)); + IERC20 token = IERC20(_getTokenAddress(symbol)); // Transfer the amount and gas payment amount from the msg.sender. token.safeTransferFrom(msg.sender, address(this), amount + gasPaymentAmount); @@ -354,8 +353,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, if (token == address(0)) revert TokenNotSupported(); if (!approved[token]) { - IERC20Upgradeable(token).safeApprove(address(gateway), type(uint256).max); - IERC20Upgradeable(token).safeApprove(address(gasService), type(uint256).max); + IERC20(token).safeApprove(address(gateway), type(uint256).max); + IERC20(token).safeApprove(address(gasService), type(uint256).max); approved[token] = true; } } @@ -375,7 +374,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, uint256 amount ) internal override { address token = _getTokenAddress(tokenSymbol); - IERC20Upgradeable tokenIn = IERC20Upgradeable(token); + IERC20 tokenIn = IERC20(token); (Commands command, bytes memory data) = abi.decode(payload, (Commands, bytes)); if (command == Commands.SendToken) { @@ -425,7 +424,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, } function _sendToken(address token, uint256 amount, address destination) internal { - IERC20Upgradeable(token).safeTransfer(destination, amount); + IERC20(token).safeTransfer(destination, amount); } function _sendNative(address token, uint256 amount, address destination) internal { @@ -453,8 +452,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, params.amountIn = amount; params.recipient = address(this); - IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); - IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + IERC20 tokenSwapIn = IERC20(token); + IERC20 tokenSwapOut = IERC20(tokenOut); uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); @@ -490,8 +489,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, (, tokenOut,) = params.path.decodeLastPool(); - IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); - IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + IERC20 tokenSwapIn = IERC20(token); + IERC20 tokenSwapOut = IERC20(tokenOut); uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); @@ -522,8 +521,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, params.amountOut = amountOut; params.amountInMaximum = amount; - IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); - IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + IERC20 tokenSwapIn = IERC20(token); + IERC20 tokenSwapOut = IERC20(tokenOut); uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); @@ -560,8 +559,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, (tokenOut,,) = params.path.decodeFirstPool(); - IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); - IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + IERC20 tokenSwapIn = IERC20(token); + IERC20 tokenSwapOut = IERC20(tokenOut); uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); @@ -588,8 +587,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, tokenOut = path[path.length - 1]; - IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); - IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + IERC20 tokenSwapIn = IERC20(token); + IERC20 tokenSwapOut = IERC20(tokenOut); uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); @@ -617,8 +616,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, tokenOut = path[path.length - 1]; - IERC20Upgradeable tokenSwapIn = IERC20Upgradeable(token); - IERC20Upgradeable tokenSwapOut = IERC20Upgradeable(tokenOut); + IERC20 tokenSwapIn = IERC20(token); + IERC20 tokenSwapOut = IERC20(tokenOut); uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); From 81c9b10f973dee591666e1602ce8c99fffded60b Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 8 Aug 2024 19:37:37 +0200 Subject: [PATCH 11/17] Switch to SafeERC20 --- CCTPRelayer/src/CCTPRelayer.sol | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/CCTPRelayer/src/CCTPRelayer.sol b/CCTPRelayer/src/CCTPRelayer.sol index b6bb355..d27773c 100644 --- a/CCTPRelayer/src/CCTPRelayer.sol +++ b/CCTPRelayer/src/CCTPRelayer.sol @@ -8,9 +8,12 @@ import {ICCTPRelayer} from "./interfaces/ICCTPRelayer.sol"; import {ITokenMessenger} from "./interfaces/ITokenMessenger.sol"; import {IMessageTransmitter} from "./interfaces/IMessageTransmitter.sol"; +import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { + using SafeERC20 for IERC20; + IERC20 public usdc; ITokenMessenger public messenger; IMessageTransmitter public transmitter; @@ -50,8 +53,8 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St function makePaymentForRelay(uint64 nonce, uint256 paymentAmount) external { if (paymentAmount == 0) revert PaymentCannotBeZero(); - // Transfer the funds from the user into the contract and fail if the transfer reverts. - if (!usdc.transferFrom(msg.sender, address(this), paymentAmount)) revert TransferFailed(); + // Transfer the funds from the user into the contract. + usdc.safeTransferFrom(msg.sender, address(this), paymentAmount); // If the transfer succeeds, emit the payment event. emit PaymentForRelay(nonce, paymentAmount); @@ -67,7 +70,7 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St if (transferAmount == 0) revert PaymentCannotBeZero(); if (feeAmount == 0) revert PaymentCannotBeZero(); // In order to save gas do the transfer only once, of both transfer amount and fee amount. - if (!usdc.transferFrom(msg.sender, address(this), transferAmount + feeAmount)) revert TransferFailed(); + usdc.safeTransferFrom(msg.sender, address(this), transferAmount + feeAmount); // Only give allowance of the transfer amount, as we want the fee amount to stay in the contract. usdc.approve(address(messenger), transferAmount); @@ -118,8 +121,6 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St uint256 outputAmount; if (inputToken == address(0)) { - IERC20 token = IERC20(inputToken); - // Native Token if (inputAmount != msg.value) revert InsufficientNativeToken(); @@ -153,7 +154,7 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St uint256 preOutputBalance = usdc.balanceOf(address(this)); // Transfer input ERC20 tokens to the contract - token.transferFrom(msg.sender, address(this), inputAmount); + token.safeTransferFrom(msg.sender, address(this), inputAmount); // Approve the swap router to spend the input tokens token.approve(swapRouter, inputAmount); @@ -173,7 +174,7 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St // Refund the remaining amount uint256 dust = postInputBalance - preInputBalance; if (dust != 0) { - token.transfer(msg.sender, dust); + token.safeTransfer(msg.sender, dust); // Revoke Approval token.approve(swapRouter, 0); @@ -212,8 +213,6 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St // Native Token if (inputAmount != msg.value) revert InsufficientNativeToken(); - IERC20 token = IERC20(inputToken); - // Get the contract's balances previous to the swap uint256 preInputBalance = address(this).balance - inputAmount; uint256 preOutputBalance = usdc.balanceOf(address(this)); @@ -244,7 +243,7 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St uint256 preOutputBalance = usdc.balanceOf(address(this)); // Transfer input ERC20 tokens to the contract - token.transferFrom(msg.sender, address(this), inputAmount); + token.safeTransferFrom(msg.sender, address(this), inputAmount); // Approve the swap router to spend the input tokens token.approve(swapRouter, inputAmount); @@ -264,7 +263,7 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St // Refund the remaining amount uint256 dust = postInputBalance - preInputBalance; if (dust != 0) { - token.transfer(msg.sender, dust); + token.safeTransfer(msg.sender, dust); // Revoke Approval token.approve(swapRouter, 0); @@ -311,8 +310,8 @@ contract CCTPRelayer is ICCTPRelayer, Initializable, UUPSUpgradeable, Ownable2St // Check that the contract has enough balance. if (usdc.balanceOf(address(this)) < amount) revert MissingBalance(); - // Check that the transfer succeeds. - if (!usdc.transfer(receiver, amount)) revert TransferFailed(); + // Transfer the amount to the receiver. + usdc.safeTransfer(receiver, amount); } fallback() external payable {} From 18ffc6bc8101d2da4c66bf2e7bc947f2b62c8e37 Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 15 Aug 2024 19:19:59 +0200 Subject: [PATCH 12/17] New router library --- .vscode/settings.json | 1 + AxelarHandler/.gitmodules | 12 - .../1/dry-run/run-1723057712.json | 31 +++ .../1/dry-run/run-1723057941.json | 31 +++ .../1/dry-run/run-latest.json | 31 +++ AxelarHandler/src/AxelarHandler.sol | 247 ++---------------- .../src/libraries/SkipSwapRouter.sol | 140 ++++++++++ 7 files changed, 259 insertions(+), 234 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 AxelarHandler/.gitmodules create mode 100644 AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057712.json create mode 100644 AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057941.json create mode 100644 AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-latest.json create mode 100644 AxelarHandler/src/libraries/SkipSwapRouter.sol diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/AxelarHandler/.gitmodules b/AxelarHandler/.gitmodules deleted file mode 100644 index 39adc4b..0000000 --- a/AxelarHandler/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std -[submodule "lib/axelar-gmp-sdk-solidity"] - path = lib/axelar-gmp-sdk-solidity - url = https://github.com/axelarnetwork/axelar-gmp-sdk-solidity -[submodule "lib/openzeppelin-contracts-upgradeable"] - path = lib/openzeppelin-contracts-upgradeable - url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057712.json b/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057712.json new file mode 100644 index 0000000..2760f89 --- /dev/null +++ b/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057712.json @@ -0,0 +1,31 @@ +{ + "transactions": [ + { + "hash": null, + "transactionType": "CREATE2", + "contractName": "BytesLib", + "contractAddress": "0xbfb201a128d08c99a6228938415b02d20b6ec4ad", + "function": null, + "arguments": null, + "transaction": { + "from": "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x27fab", + "input": "0x000000000000000000000000000000000000000000000000000000000000000061012b61003a600b82828239805160001a60731461002d57634e487b7160e01b600052600060045260246000fd5b30600052607381538281f3fe730000000000000000000000000000000000000000301460806040526004361060335760003560e01c8063593b79fe146038575b600080fd5b60676043366004607b565b604080516001600160a01b0392909216600560a21b18601483015260348201905290565b6040516072919060a9565b60405180910390f35b600060208284031215608c57600080fd5b81356001600160a01b038116811460a257600080fd5b9392505050565b600060208083528351808285015260005b8181101560d45785810183015185820160400152820160ba565b506000604082860101526040601f19601f830116850101925050509291505056fea26469706673582212202001b5e1dbec67ed81daef4e82e5dc66748bca332df0db2a80f4ac32bc7eda7664736f6c63430008120033", + "nonce": "0x0", + "chainId": "0x1" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [], + "libraries": [ + "src/libraries/Path.sol:BytesLib:0xBfB201a128D08C99a6228938415B02D20b6Ec4ad" + ], + "pending": [], + "returns": {}, + "timestamp": 1723057712, + "chain": 1, + "commit": "502ef75" +} \ No newline at end of file diff --git a/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057941.json b/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057941.json new file mode 100644 index 0000000..a96fd12 --- /dev/null +++ b/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-1723057941.json @@ -0,0 +1,31 @@ +{ + "transactions": [ + { + "hash": null, + "transactionType": "CREATE2", + "contractName": "BytesLib", + "contractAddress": "0xbfb201a128d08c99a6228938415b02d20b6ec4ad", + "function": null, + "arguments": null, + "transaction": { + "from": "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x27fab", + "input": "0x000000000000000000000000000000000000000000000000000000000000000061012b61003a600b82828239805160001a60731461002d57634e487b7160e01b600052600060045260246000fd5b30600052607381538281f3fe730000000000000000000000000000000000000000301460806040526004361060335760003560e01c8063593b79fe146038575b600080fd5b60676043366004607b565b604080516001600160a01b0392909216600560a21b18601483015260348201905290565b6040516072919060a9565b60405180910390f35b600060208284031215608c57600080fd5b81356001600160a01b038116811460a257600080fd5b9392505050565b600060208083528351808285015260005b8181101560d45785810183015185820160400152820160ba565b506000604082860101526040601f19601f830116850101925050509291505056fea26469706673582212202001b5e1dbec67ed81daef4e82e5dc66748bca332df0db2a80f4ac32bc7eda7664736f6c63430008120033", + "nonce": "0x0", + "chainId": "0x1" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [], + "libraries": [ + "src/libraries/Path.sol:BytesLib:0xBfB201a128D08C99a6228938415B02D20b6Ec4ad" + ], + "pending": [], + "returns": {}, + "timestamp": 1723057941, + "chain": 1, + "commit": "502ef75" +} \ No newline at end of file diff --git a/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-latest.json b/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-latest.json new file mode 100644 index 0000000..a96fd12 --- /dev/null +++ b/AxelarHandler/broadcast/SimulateExecuteWithToken.s.sol/1/dry-run/run-latest.json @@ -0,0 +1,31 @@ +{ + "transactions": [ + { + "hash": null, + "transactionType": "CREATE2", + "contractName": "BytesLib", + "contractAddress": "0xbfb201a128d08c99a6228938415b02d20b6ec4ad", + "function": null, + "arguments": null, + "transaction": { + "from": "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x27fab", + "input": "0x000000000000000000000000000000000000000000000000000000000000000061012b61003a600b82828239805160001a60731461002d57634e487b7160e01b600052600060045260246000fd5b30600052607381538281f3fe730000000000000000000000000000000000000000301460806040526004361060335760003560e01c8063593b79fe146038575b600080fd5b60676043366004607b565b604080516001600160a01b0392909216600560a21b18601483015260348201905290565b6040516072919060a9565b60405180910390f35b600060208284031215608c57600080fd5b81356001600160a01b038116811460a257600080fd5b9392505050565b600060208083528351808285015260005b8181101560d45785810183015185820160400152820160ba565b506000604082860101526040601f19601f830116850101925050509291505056fea26469706673582212202001b5e1dbec67ed81daef4e82e5dc66748bca332df0db2a80f4ac32bc7eda7664736f6c63430008120033", + "nonce": "0x0", + "chainId": "0x1" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [], + "libraries": [ + "src/libraries/Path.sol:BytesLib:0xBfB201a128D08C99a6228938415B02D20b6Ec4ad" + ], + "pending": [], + "returns": {}, + "timestamp": 1723057941, + "chain": 1, + "commit": "502ef75" +} \ No newline at end of file diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index 59fd678..d301cd8 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -14,14 +14,13 @@ import {UUPSUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/ import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import {ISwapRouter02} from "./interfaces/ISwapRouter02.sol"; -import {BytesLib, Path} from "./libraries/Path.sol"; +import {SkipSwapRouter} from "./libraries/SkipSwapRouter.sol"; /// @title AxelarHandler /// @notice allows to send and receive tokens to/from other chains through axelar gateway while wrapping the native tokens. /// @author Skip Protocol. contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { using SafeERC20 for IERC20; - using Path for bytes; error EmptySymbol(); error NativeSentDoesNotMatchAmounts(); @@ -43,7 +42,8 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, enum Commands { SendToken, SendNative, - Swap + Swap, + MultiSwap } bytes32 private _wETHSymbolHash; @@ -388,34 +388,31 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, _sendToken(token, amount, destination); } } else if (command == Commands.Swap) { - (address destination, uint256 amountOutMin, bool unwrapOut, bytes[] memory swaps) = - abi.decode(data, (address, uint256, bool, bytes[])); - - uint256 length = swaps.length; - for (uint256 i; i < length; ++i) { - (uint8 swapFunction, bytes memory swapPayload) = abi.decode(swaps[i], (uint8, bytes)); - - if (swapFunction == uint8(0)) { - (token, amount) = _exactInputSingleSwap(token, destination, amount, swapPayload); - } else if (swapFunction == uint8(1)) { - (token, amount) = _exactInputSwap(token, destination, amount, swapPayload); - } else if (swapFunction == uint8(2)) { - (token, amount) = _exactTokensForTokensSwap(token, destination, amount, swapPayload); - } else if (swapFunction == uint8(3)) { - (token, amount) = _exactOutputSingleSwap(token, destination, amount, swapPayload); - } else if (swapFunction == uint8(4)) { - (token, amount) = _exactOutputSwap(token, destination, amount, swapPayload); - } else if (swapFunction == uint8(5)) { - (token, amount) = _tokensForExactTokensSwap(token, destination, amount, swapPayload); + (address destination, bool unwrapOut, bytes memory swap) = abi.decode(data, (address, bool, bytes)); + + try SkipSwapRouter.swap(swapRouter, destination, tokenIn, amount, swap) returns ( + IERC20 tokenOut, uint256 amountOut + ) { + if (unwrapOut && address(tokenOut) == _getTokenAddress(wETHSymbol)) { + _sendNative(address(tokenOut), amountOut, destination); } else { - revert FunctionCodeNotSupported(); + _sendToken(address(tokenOut), amountOut, destination); } + } catch { + _sendToken(token, amount, destination); } - - if (amount < amountOutMin) revert InsufficientSwapOutput(); - if (unwrapOut && token == _getTokenAddress(wETHSymbol)) { - _sendNative(token, amount, destination); - } else { + } else if (command == Commands.MultiSwap) { + (address destination, bool unwrapOut, bytes[] memory swaps) = abi.decode(data, (address, bool, bytes[])); + + try SkipSwapRouter.multiSwap(swapRouter, destination, tokenIn, amount, swaps) returns ( + IERC20 tokenOut, uint256 amountOut + ) { + if (unwrapOut && address(tokenOut) == _getTokenAddress(wETHSymbol)) { + _sendNative(address(tokenOut), amountOut, destination); + } else { + _sendToken(address(tokenOut), amountOut, destination); + } + } catch { _sendToken(token, amount, destination); } } else { @@ -440,199 +437,5 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, } } - function _exactInputSingleSwap(address token, address destination, uint256 amount, bytes memory data) - internal - returns (address tokenOut, uint256 amountOut) - { - ISwapRouter02.ExactInputSingleParams memory params; - (tokenOut, params.fee, params.sqrtPriceLimitX96) = abi.decode(data, (address, uint24, uint160)); - - params.tokenIn = token; - params.tokenOut = tokenOut; - params.amountIn = amount; - params.recipient = address(this); - - IERC20 tokenSwapIn = IERC20(token); - IERC20 tokenSwapOut = IERC20(tokenOut); - - uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; - uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - - tokenSwapIn.safeApprove(address(swapRouter), amount); - swapRouter.exactInputSingle(params); - - uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; - amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - - if (dustIn != 0) { - tokenSwapIn.safeApprove(address(swapRouter), 0); - tokenSwapIn.safeTransfer(destination, dustIn); - } - } - - function _exactInputSwap(address token, address destination, uint256 amount, bytes memory data) - internal - returns (address tokenOut, uint256 amountOut) - { - ISwapRouter02.ExactInputParams memory params; - params.path = data; - - params.recipient = address(this); - params.amountIn = amount; - - (address tokenA,,) = params.path.decodeFirstPool(); - - if (tokenA != token) { - bytes memory tokenReplace = BytesLib.toBytes(token); - params.path = BytesLib.concat(tokenReplace, BytesLib.slice(params.path, 20, params.path.length - 20)); - } - - (, tokenOut,) = params.path.decodeLastPool(); - - IERC20 tokenSwapIn = IERC20(token); - IERC20 tokenSwapOut = IERC20(tokenOut); - - uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; - uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - - tokenSwapIn.safeApprove(address(swapRouter), amount); - swapRouter.exactInput(params); - - uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; - amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - - if (dustIn != 0) { - tokenSwapIn.safeApprove(address(swapRouter), 0); - tokenSwapIn.safeTransfer(destination, dustIn); - } - } - - function _exactOutputSingleSwap(address token, address destination, uint256 amount, bytes memory data) - internal - returns (address tokenOut, uint256 amountOut) - { - ISwapRouter02.ExactOutputSingleParams memory params; - (tokenOut, amountOut, params.fee, params.sqrtPriceLimitX96) = - abi.decode(data, (address, uint256, uint24, uint160)); - - params.tokenIn = token; - params.tokenOut = tokenOut; - params.recipient = address(this); - params.amountOut = amountOut; - params.amountInMaximum = amount; - - IERC20 tokenSwapIn = IERC20(token); - IERC20 tokenSwapOut = IERC20(tokenOut); - - uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; - uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - - tokenSwapIn.safeApprove(address(swapRouter), amount); - swapRouter.exactOutputSingle(params); - - uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; - amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - - if (dustIn != 0) { - tokenSwapIn.safeApprove(address(swapRouter), 0); - tokenSwapIn.safeTransfer(destination, dustIn); - } - } - - function _exactOutputSwap(address token, address destination, uint256 amount, bytes memory data) - internal - returns (address tokenOut, uint256 amountOut) - { - ISwapRouter02.ExactOutputParams memory params; - (amountOut, params.path) = abi.decode(data, (uint256, bytes)); - - params.recipient = address(this); - params.amountOut = amountOut; - params.amountInMaximum = amount; - - (, address tokenB,) = params.path.decodeLastPool(); - - if (tokenB != token) { - bytes memory tokenReplace = BytesLib.toBytes(token); - params.path = BytesLib.concat(BytesLib.slice(params.path, 0, params.path.length - 20), tokenReplace); - } - - (tokenOut,,) = params.path.decodeFirstPool(); - - IERC20 tokenSwapIn = IERC20(token); - IERC20 tokenSwapOut = IERC20(tokenOut); - - uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; - uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - - tokenSwapIn.safeApprove(address(swapRouter), amount); - swapRouter.exactOutput(params); - - uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; - amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - - if (dustIn != 0) { - tokenSwapIn.safeApprove(address(swapRouter), 0); - tokenSwapIn.safeTransfer(destination, dustIn); - } - } - - function _exactTokensForTokensSwap(address token, address destination, uint256 amount, bytes memory data) - internal - returns (address tokenOut, uint256 amountOut) - { - (address[] memory path) = abi.decode(data, (address[])); - - path[0] == token; - - tokenOut = path[path.length - 1]; - - IERC20 tokenSwapIn = IERC20(token); - IERC20 tokenSwapOut = IERC20(tokenOut); - - uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; - uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - - tokenSwapIn.safeApprove(address(swapRouter), amount); - swapRouter.swapExactTokensForTokens(amount, 0, path, address(this)); - - uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; - amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - - if (dustIn != 0) { - tokenSwapIn.safeApprove(address(swapRouter), 0); - tokenSwapIn.safeTransfer(destination, dustIn); - } - } - - function _tokensForExactTokensSwap(address token, address destination, uint256 amount, bytes memory data) - internal - returns (address tokenOut, uint256 amountOut) - { - address[] memory path; - (amountOut, path) = abi.decode(data, (uint256, address[])); - - path[0] == token; - - tokenOut = path[path.length - 1]; - - IERC20 tokenSwapIn = IERC20(token); - IERC20 tokenSwapOut = IERC20(tokenOut); - - uint256 preBalIn = tokenSwapIn.balanceOf(address(this)) - amount; - uint256 preBalOut = tokenSwapOut.balanceOf(address(this)); - - tokenSwapIn.safeApprove(address(swapRouter), amount); - swapRouter.swapTokensForExactTokens(amountOut, amount, path, address(this)); - - uint256 dustIn = tokenSwapIn.balanceOf(address(this)) - preBalIn; - amountOut = tokenSwapOut.balanceOf(address(this)) - preBalOut; - - if (dustIn != 0) { - tokenSwapIn.safeApprove(address(swapRouter), 0); - tokenSwapIn.safeTransfer(destination, dustIn); - } - } - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} } diff --git a/AxelarHandler/src/libraries/SkipSwapRouter.sol b/AxelarHandler/src/libraries/SkipSwapRouter.sol new file mode 100644 index 0000000..70e228d --- /dev/null +++ b/AxelarHandler/src/libraries/SkipSwapRouter.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: UNLICENSED +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ISwapRouter02} from "../interfaces/ISwapRouter02.sol"; +import {BytesLib, Path} from "./Path.sol"; + +pragma solidity >= 0.8.18; + +library SkipSwapRouter { + using SafeERC20 for IERC20; + using Path for bytes; + + error InsufficientOutputAmount(); + + enum SwapCommands { + ExactInputSingle, + ExactInput, + ExactTokensForTokens, + ExactOutputSingle, + ExactOutput, + TokensForExactTokens + } + + function multiSwap( + ISwapRouter02 router, + address destination, + IERC20 inputToken, + uint256 amountIn, + bytes[] memory swaps + ) external returns (IERC20 outputToken, uint256 amountOut) { + outputToken = inputToken; + amountOut = amountIn; + + uint256 numSwaps = swaps.length; + for (uint256 i; i < numSwaps; i++) { + // The output token and amount of each iteration is the input token and amount of the next. + (outputToken, amountOut) = swap(router, destination, outputToken, amountOut, swaps[i]); + } + } + + function swap(ISwapRouter02 router, address destination, IERC20 inputToken, uint256 amountIn, bytes memory payload) + public + returns (IERC20 outputToken, uint256 amountOut) + { + (SwapCommands command, address tokenOut, uint256 amountOut, bytes memory swapData) = + abi.decode(payload, (SwapCommands, address, uint256, bytes)); + + outputToken = IERC20(tokenOut); + + uint256 preBalIn = inputToken.balanceOf(address(this)) - amountIn; + uint256 preBalOut = outputToken.balanceOf(address(this)); + + inputToken.forceApprove(address(router), amountIn); + + if (command == SwapCommands.ExactInputSingle) { + ISwapRouter02.ExactInputSingleParams memory params; + params.tokenIn = address(inputToken); + params.tokenOut = tokenOut; + params.recipient = address(this); + params.amountIn = amountIn; + params.amountOutMinimum = amountOut; + + (params.fee, params.sqrtPriceLimitX96) = abi.decode(swapData, (uint24, uint160)); + + router.exactInputSingle(params); + } else if (command == SwapCommands.ExactInput) { + ISwapRouter02.ExactInputParams memory params; + params.path = _fixPath(address(inputToken), tokenOut, swapData); + params.recipient = address(this); + params.amountIn = amountIn; + params.amountOutMinimum = amountOut; + + router.exactInput(params); + } else if (command == SwapCommands.ExactTokensForTokens) { + address[] memory path = _fixPath(address(inputToken), tokenOut, abi.decode(swapData, (address[]))); + + router.swapExactTokensForTokens(amountIn, amountOut, path, address(this)); + } else if (command == SwapCommands.ExactOutputSingle) { + ISwapRouter02.ExactOutputSingleParams memory params; + params.tokenIn = address(inputToken); + params.tokenOut = tokenOut; + params.recipient = address(this); + params.amountInMaximum = amountIn; + params.amountOut = amountOut; + + (params.fee, params.sqrtPriceLimitX96) = abi.decode(swapData, (uint24, uint160)); + + router.exactOutputSingle(params); + } else if (command == SwapCommands.ExactOutput) { + ISwapRouter02.ExactOutputParams memory params; + params.path = _fixPath(tokenOut, address(inputToken), swapData); + params.recipient = address(this); + params.amountInMaximum = amountIn; + params.amountOut = amountOut; + + router.exactOutput(params); + } else if (command == SwapCommands.TokensForExactTokens) { + address[] memory path = _fixPath(tokenOut, address(inputToken), abi.decode(swapData, (address[]))); + + router.swapTokensForExactTokens(amountOut, amountIn, path, address(this)); + } + + if (amountOut < outputToken.balanceOf(address(this)) - preBalOut) { + revert InsufficientOutputAmount(); + } + + uint256 dust = inputToken.balanceOf(address(this)) - preBalIn; + if (dust != 0) { + inputToken.forceApprove(address(router), 0); + inputToken.safeTransfer(destination, dust); + } + } + + function _fixPath(address tokenA, address tokenB, bytes memory path) internal pure returns (bytes memory) { + (address decodedA,,) = path.decodeFirstPool(); + if (decodedA != tokenA) { + path = BytesLib.concat(BytesLib.toBytes(tokenA), BytesLib.slice(path, 20, path.length - 20)); + } + + (, address decodedB,) = path.decodeLastPool(); + if (decodedB != tokenB) { + path = BytesLib.concat(BytesLib.slice(path, 0, path.length - 20), BytesLib.toBytes(tokenB)); + } + + return path; + } + + function _fixPath(address tokenA, address tokenB, address[] memory path) internal pure returns (address[] memory) { + if (path[0] != tokenA) { + path[0] = tokenA; + } + + uint256 lastElement = path.length - 1; + if (path[lastElement] != tokenB) { + path[lastElement] = tokenB; + } + + return path; + } +} From 70c4c12306ce36ede5faca0f7ebe23ca3256e4c4 Mon Sep 17 00:00:00 2001 From: Manu Date: Mon, 19 Aug 2024 19:15:01 +0200 Subject: [PATCH 13/17] Adapt tests to use new SkipSwapRouter and bugfixes --- .../script/SimulateExecuteWithToken.s.sol | 14 ++--- .../src/libraries/SkipSwapRouter.sol | 21 ++++--- AxelarHandler/test/AxelarHandler.t.sol | 57 ++++++++++++------- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/AxelarHandler/script/SimulateExecuteWithToken.s.sol b/AxelarHandler/script/SimulateExecuteWithToken.s.sol index 988638d..f3c02c7 100644 --- a/AxelarHandler/script/SimulateExecuteWithToken.s.sol +++ b/AxelarHandler/script/SimulateExecuteWithToken.s.sol @@ -50,21 +50,15 @@ contract SimulateExecuteWithToken is Script, Test { mockGateway.saveTokenAddress(tokenInputSymbol, tokenIn); deal(address(tokenIn), address(handler), amountIn); - bytes[] memory swaps = new bytes[](1); - swaps[0] = abi.encode(uint8(0), abi.encode(tokenOut, uint24(3000), uint160(0))); - bytes memory examplePayload = abi.encode(uint8(2), abi.encode(destination, amountOutMin, unwrap, swaps)); + bytes memory swapPayload = abi.encode(uint8(0), tokenOut, amountOutMin, abi.encode(uint24(3000), uint160(0))); + bytes memory payload = abi.encode(uint8(2), abi.encode(destination, unwrap, swapPayload)); - console2.logBytes(examplePayload); + console2.logBytes(payload); console2.log("Token Out Destination Balance Before: %s", IERC20(tokenOut).balanceOf(destination)); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), - "osmosis-7", - "mock_address", - examplePayload, - tokenInputSymbol, - amountIn + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, tokenInputSymbol, amountIn ); console2.log("Token Out Destination Balance After: %s", IERC20(tokenOut).balanceOf(destination)); diff --git a/AxelarHandler/src/libraries/SkipSwapRouter.sol b/AxelarHandler/src/libraries/SkipSwapRouter.sol index 70e228d..ee674b3 100644 --- a/AxelarHandler/src/libraries/SkipSwapRouter.sol +++ b/AxelarHandler/src/libraries/SkipSwapRouter.sol @@ -4,6 +4,8 @@ import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/ import {ISwapRouter02} from "../interfaces/ISwapRouter02.sol"; import {BytesLib, Path} from "./Path.sol"; +import {console} from "forge-std/Test.sol"; + pragma solidity >= 0.8.18; library SkipSwapRouter { @@ -40,7 +42,7 @@ library SkipSwapRouter { function swap(ISwapRouter02 router, address destination, IERC20 inputToken, uint256 amountIn, bytes memory payload) public - returns (IERC20 outputToken, uint256 amountOut) + returns (IERC20 outputToken, uint256 outputAmount) { (SwapCommands command, address tokenOut, uint256 amountOut, bytes memory swapData) = abi.decode(payload, (SwapCommands, address, uint256, bytes)); @@ -65,16 +67,17 @@ library SkipSwapRouter { router.exactInputSingle(params); } else if (command == SwapCommands.ExactInput) { ISwapRouter02.ExactInputParams memory params; - params.path = _fixPath(address(inputToken), tokenOut, swapData); + // params.path = _fixPath(address(inputToken), tokenOut, swapData); + params.path = swapData; params.recipient = address(this); params.amountIn = amountIn; params.amountOutMinimum = amountOut; router.exactInput(params); } else if (command == SwapCommands.ExactTokensForTokens) { - address[] memory path = _fixPath(address(inputToken), tokenOut, abi.decode(swapData, (address[]))); + //address[] memory path = _fixPath(address(inputToken), tokenOut, abi.decode(swapData, (address[]))); - router.swapExactTokensForTokens(amountIn, amountOut, path, address(this)); + router.swapExactTokensForTokens(amountIn, amountOut, abi.decode(swapData, (address[])), address(this)); } else if (command == SwapCommands.ExactOutputSingle) { ISwapRouter02.ExactOutputSingleParams memory params; params.tokenIn = address(inputToken); @@ -88,19 +91,21 @@ library SkipSwapRouter { router.exactOutputSingle(params); } else if (command == SwapCommands.ExactOutput) { ISwapRouter02.ExactOutputParams memory params; - params.path = _fixPath(tokenOut, address(inputToken), swapData); + // params.path = _fixPath(tokenOut, address(inputToken), swapData); + params.path = swapData; params.recipient = address(this); params.amountInMaximum = amountIn; params.amountOut = amountOut; router.exactOutput(params); } else if (command == SwapCommands.TokensForExactTokens) { - address[] memory path = _fixPath(tokenOut, address(inputToken), abi.decode(swapData, (address[]))); + //address[] memory path = _fixPath(tokenOut, address(inputToken), abi.decode(swapData, (address[]))); - router.swapTokensForExactTokens(amountOut, amountIn, path, address(this)); + router.swapTokensForExactTokens(amountOut, amountIn, abi.decode(swapData, (address[])), address(this)); } - if (amountOut < outputToken.balanceOf(address(this)) - preBalOut) { + outputAmount = outputToken.balanceOf(address(this)) - preBalOut; + if (outputAmount < amountOut) { revert InsufficientOutputAmount(); } diff --git a/AxelarHandler/test/AxelarHandler.t.sol b/AxelarHandler/test/AxelarHandler.t.sol index d825342..3e91a0b 100644 --- a/AxelarHandler/test/AxelarHandler.t.sol +++ b/AxelarHandler/test/AxelarHandler.t.sol @@ -397,10 +397,8 @@ contract AxelarHandlerTest is Test { assertEq(inputToken.balanceOf(destination), 0, "Destination input token balance before"); assertEq(outputToken.balanceOf(destination), 0, "Destination output token balance before"); - bytes[] memory swaps = new bytes[](1); - swaps[0] = abi.encode(uint8(0), abi.encode(tokenOut, uint24(3000), uint160(0))); - - bytes memory payload = abi.encode(uint8(2), abi.encode(destination, amountOutMin, unwrap, swaps)); + bytes memory swapPayload = abi.encode(uint8(0), tokenOut, amountOutMin, abi.encode(uint24(3000), uint160(0))); + bytes memory payload = abi.encode(uint8(2), abi.encode(destination, unwrap, swapPayload)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn @@ -453,9 +451,9 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - bytes[] memory swaps = new bytes[](1); - swaps[0] = abi.encode(uint8(1), abi.encodePacked(address(inputToken), uint24(500), address(outputToken))); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOutMinimum, false, swaps)); + bytes memory path = abi.encodePacked(address(inputToken), uint24(500), address(outputToken)); + bytes memory swapPayload = abi.encode(uint8(1), address(outputToken), amountOutMinimum, path); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount @@ -496,9 +494,8 @@ contract AxelarHandlerTest is Test { path[0] = address(inputToken); path[1] = address(outputToken); - bytes[] memory swaps = new bytes[](1); - swaps[0] = abi.encode(uint8(2), abi.encode(path)); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, minOutput, false, swaps)); + bytes memory swapPayload = abi.encode(uint8(2), address(outputToken), minOutput, abi.encode(path)); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount @@ -535,9 +532,9 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - bytes[] memory swaps = new bytes[](1); - swaps[0] = abi.encode(uint8(3), abi.encode(address(outputToken), uint256(amountOut), uint24(3000), uint160(0))); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOut, false, swaps)); + bytes memory swapPayload = + abi.encode(uint8(3), address(outputToken), amountOut, abi.encode(uint24(3000), uint160(0))); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount @@ -574,12 +571,9 @@ contract AxelarHandlerTest is Test { MockGateway mockGateway = MockGateway(address(gateway)); mockGateway.saveTokenAddress(symbol, address(inputToken)); - bytes[] memory swaps = new bytes[](1); - swaps[0] = abi.encode( - uint8(4), - abi.encode(uint256(amountOut), abi.encodePacked(address(outputToken), uint24(500), address(inputToken))) - ); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOut, false, swaps)); + bytes memory path = abi.encodePacked(address(outputToken), uint24(500), address(inputToken)); + bytes memory swapPayload = abi.encode(uint8(4), address(outputToken), amountOut, path); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount @@ -620,9 +614,8 @@ contract AxelarHandlerTest is Test { path[0] = address(inputToken); path[1] = address(outputToken); - bytes[] memory swaps = new bytes[](1); - swaps[0] = abi.encode(uint8(5), abi.encode(amountOut, path)); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, amountOut, false, swaps)); + bytes memory swapPayload = abi.encode(uint8(5), address(outputToken), amountOut, abi.encode(path)); + bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); handler.executeWithToken( keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount @@ -635,6 +628,26 @@ contract AxelarHandlerTest is Test { assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); } + function test_executeWithToken_custom() public forked { + uint256 inputAmount = 5 ether; + string memory symbol = "WETH"; + IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + + deployCodeTo("MockGateway.sol", address(gateway)); + MockGateway mockGateway = MockGateway(address(gateway)); + mockGateway.saveTokenAddress(symbol, address(inputToken)); + + deal(address(inputToken), address(this), inputAmount); + inputToken.transfer(address(handler), inputAmount); + + bytes memory payload = + hex"000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000ef211076b8d8b46797e09c9a374fb4cdc1df09160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"; + + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + ); + } + // function test_executeWithToken_swap_refundDust() public { // uint256 inputAmount = 4.95 ether; // uint256 dust = 0.05 ether; From 6d3e91f76cdb0d175190112ccffa7d6a1c15097e Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 22 Aug 2024 05:21:03 +0200 Subject: [PATCH 14/17] Swap Router tests --- AxelarHandler/src/AxelarHandler.sol | 11 +- .../src/libraries/SkipSwapRouter.sol | 12 +- AxelarHandler/test/AxelarHandler.t.sol | 723 ++++++++++++------ 3 files changed, 519 insertions(+), 227 deletions(-) diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index d301cd8..956efc4 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.18; +import {console} from "forge-std/console.sol"; + import {IWETH} from "./interfaces/IWETH.sol"; import {IAxelarGasService} from "lib/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol"; @@ -361,14 +363,12 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, /// @notice Internal function called by the AxelarExecutor when a GMP call is made to this contract. /// @notice Receives the tokens and unwraps them if it's wrapped native currency. - /// @param sourceChain the name of the chain where the GMP message originated. - /// @param sourceAddress the address where the GMP message originated. - /// @param sourceChain the payload that was sent along with the GMP message. + /// @param payload the payload that was sent along with the GMP message. /// @param tokenSymbol the symbol of the tokens received. /// @param amount the amount of tokens received. function _executeWithToken( - string calldata sourceChain, - string calldata sourceAddress, + string calldata, // sourceChain + string calldata, // sourceAddress bytes calldata payload, string calldata tokenSymbol, uint256 amount @@ -403,6 +403,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, } } else if (command == Commands.MultiSwap) { (address destination, bool unwrapOut, bytes[] memory swaps) = abi.decode(data, (address, bool, bytes[])); + console.log("debug"); try SkipSwapRouter.multiSwap(swapRouter, destination, tokenIn, amount, swaps) returns ( IERC20 tokenOut, uint256 amountOut diff --git a/AxelarHandler/src/libraries/SkipSwapRouter.sol b/AxelarHandler/src/libraries/SkipSwapRouter.sol index ee674b3..d7becdc 100644 --- a/AxelarHandler/src/libraries/SkipSwapRouter.sol +++ b/AxelarHandler/src/libraries/SkipSwapRouter.sol @@ -67,7 +67,7 @@ library SkipSwapRouter { router.exactInputSingle(params); } else if (command == SwapCommands.ExactInput) { ISwapRouter02.ExactInputParams memory params; - // params.path = _fixPath(address(inputToken), tokenOut, swapData); + params.path = _fixPath(address(inputToken), tokenOut, swapData); params.path = swapData; params.recipient = address(this); params.amountIn = amountIn; @@ -75,9 +75,9 @@ library SkipSwapRouter { router.exactInput(params); } else if (command == SwapCommands.ExactTokensForTokens) { - //address[] memory path = _fixPath(address(inputToken), tokenOut, abi.decode(swapData, (address[]))); + address[] memory path = _fixPath(address(inputToken), tokenOut, abi.decode(swapData, (address[]))); - router.swapExactTokensForTokens(amountIn, amountOut, abi.decode(swapData, (address[])), address(this)); + router.swapExactTokensForTokens(amountIn, amountOut, path, address(this)); } else if (command == SwapCommands.ExactOutputSingle) { ISwapRouter02.ExactOutputSingleParams memory params; params.tokenIn = address(inputToken); @@ -91,7 +91,7 @@ library SkipSwapRouter { router.exactOutputSingle(params); } else if (command == SwapCommands.ExactOutput) { ISwapRouter02.ExactOutputParams memory params; - // params.path = _fixPath(tokenOut, address(inputToken), swapData); + params.path = _fixPath(tokenOut, address(inputToken), swapData); params.path = swapData; params.recipient = address(this); params.amountInMaximum = amountIn; @@ -99,9 +99,9 @@ library SkipSwapRouter { router.exactOutput(params); } else if (command == SwapCommands.TokensForExactTokens) { - //address[] memory path = _fixPath(tokenOut, address(inputToken), abi.decode(swapData, (address[]))); + address[] memory path = _fixPath(address(inputToken), address(tokenOut), abi.decode(swapData, (address[]))); - router.swapTokensForExactTokens(amountOut, amountIn, abi.decode(swapData, (address[])), address(this)); + router.swapTokensForExactTokens(amountOut, amountIn, path, address(this)); } outputAmount = outputToken.balanceOf(address(this)) - preBalOut; diff --git a/AxelarHandler/test/AxelarHandler.t.sol b/AxelarHandler/test/AxelarHandler.t.sol index 3e91a0b..f73cf60 100644 --- a/AxelarHandler/test/AxelarHandler.t.sol +++ b/AxelarHandler/test/AxelarHandler.t.sol @@ -21,6 +21,7 @@ contract AxelarHandlerTest is Test { string public constant FORK_CHAIN = "mainnet"; address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public immutable ALICE = makeAddr("ALICE"); address public immutable BOB = makeAddr("BOB"); @@ -37,7 +38,7 @@ contract AxelarHandlerTest is Test { env.setEnv(1); vm.makePersistent(address(env)); - vm.createSelectFork(vm.rpcUrl(FORK_CHAIN)); + vm.createSelectFork(vm.rpcUrl(FORK_CHAIN), 20581168); gateway = IAxelarGateway(env.gateway()); address gasService = env.gasService(); @@ -88,7 +89,7 @@ contract AxelarHandlerTest is Test { _; } - function test_sendNativeToken() public forked { + function test_forked_sendNativeToken() public forked { vm.deal(ALICE, 10 ether); assertEq(ALICE.balance, 10 ether, "Native balance before sending."); @@ -100,7 +101,7 @@ contract AxelarHandlerTest is Test { assertEq(address(handler).balance, 0, "Ether left in the contract."); } - function test_sendNativeToken_NoAmount() public forked { + function test_forked_sendNativeToken_NoAmount() public forked { vm.deal(ALICE, 10 ether); assertEq(ALICE.balance, 10 ether, "Native balance before sending."); @@ -110,7 +111,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_sendERC20Token() public forked { + function test_forked_sendERC20Token() public forked { string memory symbol = "WETH"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); @@ -126,7 +127,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(address(handler)), 0, "Tokens left in the contract."); } - function test_sendERC20Token_WrongSymbol() public forked { + function test_forked_sendERC20Token_WrongSymbol() public forked { string memory symbol = "WBTCx"; vm.startPrank(ALICE); vm.expectRevert(AxelarHandler.TokenNotSupported.selector); @@ -134,7 +135,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_sendERC20Token_NoAllowance() public forked { + function test_forked_sendERC20Token_NoAllowance() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); @@ -149,7 +150,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferNativeToken() public forked { + function test_forked_gmpTransferNativeToken() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -163,7 +164,7 @@ contract AxelarHandlerTest is Test { assertEq(address(handler).balance, 0, "Ether left in the contract."); } - function test_gmpTransferNativeToken_ZeroGas() public forked { + function test_forked_gmpTransferNativeToken_ZeroGas() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -175,7 +176,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferNativeToken_ZeroAmount() public forked { + function test_forked_gmpTransferNativeToken_ZeroAmount() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -187,7 +188,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferNativeToken_AmountMismatch() public forked { + function test_forked_gmpTransferNativeToken_AmountMismatch() public forked { vm.deal(ALICE, 100 ether); assertEq(ALICE.balance, 100 ether, "Native balance before sending."); @@ -199,7 +200,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferERC20Token() public forked { + function test_forked_gmpTransferERC20Token() public forked { vm.deal(ALICE, 25 ether); assertEq(ALICE.balance, 25 ether, "Native balance before sending."); @@ -222,7 +223,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(address(handler)), 0, "Tokens left in the contract."); } - function test_gmpTransferERC20Token_GasMismatch() public forked { + function test_forked_gmpTransferERC20Token_GasMismatch() public forked { vm.deal(ALICE, 0.5 ether); assertEq(ALICE.balance, 0.5 ether, "Native balance before sending."); @@ -241,7 +242,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferERC20Token_ZeroGas() public forked { + function test_forked_gmpTransferERC20Token_ZeroGas() public forked { vm.deal(ALICE, 0.5 ether); assertEq(ALICE.balance, 0.5 ether, "Native balance before sending."); @@ -260,7 +261,7 @@ contract AxelarHandlerTest is Test { vm.stopPrank(); } - function test_gmpTransferERC20TokenGasTokenPayment() public forked { + function test_forked_gmpTransferERC20TokenGasTokenPayment() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WBTC"); @@ -279,7 +280,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(address(handler)), 0, "Tokens left in the contract."); } - function test_executeWithToken_nonunwrap_nonWETH() public forked { + function test_forked_executeWithToken_nonunwrap_nonWETH() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WBTC"); @@ -306,7 +307,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(ALICE), 100 ether, "Alice balance after"); } - function test_executeWithToken_unwrap_nonWETH() public forked { + function test_forked_executeWithToken_unwrap_nonWETH() public forked { string memory symbol = "WBTC"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WBTC"); @@ -333,7 +334,7 @@ contract AxelarHandlerTest is Test { assertEq(token.balanceOf(ALICE), 100 ether, "Alice balance after"); } - function test_executeWithToken_unwrap_WETH() public forked { + function test_forked_executeWithToken_unwrap_WETH() public forked { string memory symbol = "WETH"; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); vm.label(address(token), "WETH"); @@ -361,294 +362,594 @@ contract AxelarHandlerTest is Test { assertEq(ALICE.balance, 100 ether, "Alice native balance after"); } - function test_executeWithToken_exactInputSingleSwap_Fork() public forked { - string memory symbol = "WETH"; - address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbol); - vm.label(address(tokenIn), symbol); - - address tokenOut = USDC; - vm.label(address(tokenOut), "USDC"); - + function test_forked_executeWithToken_exactInputSingleSwap() public forked { uint256 amountIn = 1 ether; // 1 WETH uint256 amountOutMin = 1_000 * 1e6; // 1,000 USDC address destination = ALICE; bool unwrap = false; - _execute_exactInputSingleSwap(symbol, tokenIn, tokenOut, amountIn, amountOutMin, destination, unwrap); + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); + + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); + + _execute_exactInputSingleSwap(symbolIn, tokenIn, tokenOut, amountIn, amountOutMin, destination, unwrap); } - function _execute_exactInputSingleSwap( - string memory symbol, - address tokenIn, - address tokenOut, - uint256 amountIn, - uint256 amountOutMin, - address destination, - bool unwrap - ) internal { - _mockGateway(symbol, tokenIn, amountIn); + function test_forked_executeWithToken_exactInputSingleSwap_unwrap() public forked { + uint256 amountIn = 5_000 ether; // 5,000 USDT + uint256 amountOutMin = 1.5 ether; // 1.5 ETH + address destination = ALICE; + bool unwrap = true; - IERC20 inputToken = IERC20(tokenIn); - IERC20 outputToken = IERC20(tokenOut); + string memory symbolIn = "USDT"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); - assertEq(inputToken.balanceOf(address(handler)), amountIn, "Handler input token balance before"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + string memory symbolOut = "WETH"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); - assertEq(inputToken.balanceOf(destination), 0, "Destination input token balance before"); - assertEq(outputToken.balanceOf(destination), 0, "Destination output token balance before"); + _execute_exactInputSingleSwap(symbolIn, tokenIn, tokenOut, amountIn, amountOutMin, destination, unwrap); + } + + function test_forked_executeWithToken_exactInputSingleSwap_unwrap_noWETH() public forked { + uint256 amountIn = 1 ether; // 0.1 WBTC + uint256 amountOutMin = 1_000 * 1e6; // 1,000 USDCs + address destination = ALICE; + bool unwrap = true; - bytes memory swapPayload = abi.encode(uint8(0), tokenOut, amountOutMin, abi.encode(uint24(3000), uint160(0))); - bytes memory payload = abi.encode(uint8(2), abi.encode(destination, unwrap, swapPayload)); + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); + + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); + + _mockGateway(symbolIn, tokenIn, amountIn); + + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); + _assertPreSwap(inputToken, outputToken, amountIn, destination); + + bytes memory payload; + { + bytes memory swapPayload = _build_exactInputSingle(tokenOut, amountOutMin, 3000); + payload = _build_executeWithToken(destination, unwrap, swapPayload); + } handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbolIn, amountIn ); - assertEq(inputToken.balanceOf(destination), 0, "Destination input token balance after"); - if (unwrap) { - assertGt(destination.balance, amountOutMin, "Destination output native balance after"); - assertEq(address(handler).balance, 0, "Handler native token balance after"); - } else { - assertGt(IERC20(tokenOut).balanceOf(destination), amountOutMin, "Destination output token balance after"); - } - assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); + _assertPostSwapExactInput(inputToken, outputToken, amountOutMin, destination, false); } - function _mockGateway(string memory symbol, address tokenIn, uint256 amountIn) internal { - deal(tokenIn, address(handler), amountIn); + function test_forked_executeWithToken_exactInputSwap() public forked { + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOutMin = 1_000 * 1e6; // 1,000 USDC + address destination = ALICE; + bool unwrap = false; - if (isForked) { - deployCodeTo("MockGateway.sol", address(gateway)); - } + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); - MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, tokenIn); + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); + + bytes memory path = abi.encodePacked(tokenIn, uint24(500), tokenOut); + _execute_exactInputSwap(symbolIn, tokenIn, tokenOut, amountIn, amountOutMin, destination, unwrap, path); } - function test_executeWithToken_exactInputSwap() public forked { - uint256 inputAmount = 1 ether; - uint256 amountOutMinimum = 1000 * 1e6; // 1000 USDC + function test_forked_executeWithToken_exactInputSwap_insufficientOutput() public forked { + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOutMin = 4_000 * 1e6; // 1,000 USDC + address destination = ALICE; + bool unwrap = false; - string memory symbol = "WETH"; - IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); - vm.label(address(inputToken), "WETH"); + string memory symbolIn = "WETH"; - IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - vm.label(address(outputToken), "USDC"); + IERC20 inputToken; + IERC20 outputToken; + bytes memory path; + { + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); + inputToken = IERC20(tokenIn); - deal(address(inputToken), address(this), inputAmount); - inputToken.transfer(address(handler), inputAmount); + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); + outputToken = IERC20(tokenOut); - assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + _mockGateway(symbolIn, tokenIn, amountIn); - assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); - assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + path = abi.encodePacked(tokenIn, uint24(500), tokenOut); + } - deployCodeTo("MockGateway.sol", address(gateway)); - MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, address(inputToken)); + _assertPreSwap(inputToken, outputToken, amountIn, destination); - bytes memory path = abi.encodePacked(address(inputToken), uint24(500), address(outputToken)); - bytes memory swapPayload = abi.encode(uint8(1), address(outputToken), amountOutMinimum, path); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); + bytes memory swapPayload = _build_exactInput(address(outputToken), amountOutMin, path); + bytes memory payload = _build_executeWithToken(destination, unwrap, swapPayload); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbolIn, amountIn ); - assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); - assertGt(outputToken.balanceOf(ALICE), amountOutMinimum, "Alice output token balance after"); - assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); + _assertNoDust(inputToken); + _assertNoDust(outputToken); + _assertNoAllowance(inputToken); + + assertEq(inputToken.balanceOf(destination), amountIn, "Input not refunded upon failed swap"); + assertEq(outputToken.balanceOf(destination), 0, "Existing output after failed swap"); } - function test_executeWithToken_exactTokensForTokensSwap() public forked { - uint256 inputAmount = 5 ether; - uint256 minOutput = 10_000 * 1e6; + function test_forked_executeWithToken_exactTokensForTokensSwap() public forked { + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOutMin = 1_000 * 1e6; // 1,000 USDC + address destination = ALICE; + bool unwrap = false; - string memory symbol = "WETH"; - IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); - vm.label(address(inputToken), "WETH"); + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); - IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - vm.label(address(inputToken), "USDC"); + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); - deal(address(inputToken), address(this), inputAmount); - inputToken.transfer(address(handler), inputAmount); + address[] memory path = new address[](2); + path[0] = tokenIn; + path[1] = tokenOut; - assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + _execute_exactTokensForTokens(symbolIn, tokenIn, tokenOut, amountIn, amountOutMin, destination, unwrap, path); + } - assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); - assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + function test_forked_executeWithToken_exactOutputSingleSwap() public forked { + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOut = 1_000 * 1e6; // 1,000 USDC + address destination = ALICE; + bool unwrap = false; - deployCodeTo("MockGateway.sol", address(gateway)); - MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, address(inputToken)); + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); + + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); + + _execute_exactOutputSingleSwap(symbolIn, tokenIn, tokenOut, amountIn, amountOut, destination, unwrap); + } + + function test_forked_executeWithToken_exactOutputSwap() public forked { + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOut = 1_000 * 1e6; // 1,000 USDC + address destination = ALICE; + bool unwrap = false; + + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); + + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); + + bytes memory path = abi.encodePacked(tokenOut, uint24(500), tokenIn); + _execute_exactOutputSwap(symbolIn, tokenIn, tokenOut, amountIn, amountOut, destination, unwrap, path); + } + + function test_forked_executeWithToken_tokensForExactTokensSwap() public forked { + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOut = 1_000 * 1e6; // 1,000 USDC + address destination = ALICE; + bool unwrap = false; + + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); + + string memory symbolOut = "USDC"; + address tokenOut = IAxelarGateway(gateway).tokenAddresses(symbolOut); + vm.label(address(tokenOut), symbolOut); address[] memory path = new address[](2); - path[0] = address(inputToken); - path[1] = address(outputToken); + path[0] = tokenIn; + path[1] = tokenOut; + + _execute_tokensForExactTokens(symbolIn, tokenIn, tokenOut, amountIn, amountOut, destination, unwrap, path); + } + + function test_forked_executeWithToken_multiSwap() public forked { + uint256 amountIn = 1 ether; // 1 WETH + uint256 amountOut = 2_000 * 1e6; // 2,000 USDT + address destination = ALICE; + bool unwrap = false; + + string memory symbolIn = "WETH"; + address tokenIn = IAxelarGateway(gateway).tokenAddresses(symbolIn); + vm.label(address(tokenIn), symbolIn); - bytes memory swapPayload = abi.encode(uint8(2), address(outputToken), minOutput, abi.encode(path)); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); + address tokenOut = IAxelarGateway(gateway).tokenAddresses("USDT"); + + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); + + bytes memory payload; + { + bytes[] memory swaps = new bytes[](2); + swaps[0] = _build_exactInputSingle(USDC, 2_000 * 1e6, 500); + swaps[1] = _build_exactOutputSingle(tokenOut, amountOut, 500); + + payload = _build_executeWithToken_multiswap(destination, unwrap, swaps); + } + + _mockGateway(symbolIn, tokenIn, amountIn); + + _assertPreSwap(inputToken, outputToken, amountIn, destination); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbolIn, amountIn ); - assertEq(inputToken.balanceOf(ALICE), 0, "User got refunded input"); - assertGt(outputToken.balanceOf(ALICE), minOutput, "User balance didn't increase"); - assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); - assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); - } + { + IERC20[] memory tokens = new IERC20[](3); + tokens[0] = inputToken; + tokens[1] = IERC20(USDC); + tokens[2] = outputToken; - function test_executeWithToken_exactOutputSingleSwap() public forked { - uint256 inputAmount = 1 ether; - uint256 amountOut = 1000 * 1e6; // 1000 USDC + _assertNoDust(tokens); + _assertNoAllowance(tokens); + } + assertEq(outputToken.balanceOf(destination), amountOut, "Output token below minimum"); + assertEq(inputToken.balanceOf(destination), 0, "Input token refunded"); + assertNotEq(IERC20(USDC).balanceOf(destination), 0); + + console2.log("Intermediate refund: ", IERC20(USDC).balanceOf(destination)); + } + + function test_forked_executeWithToken_custom() public forked { + uint256 inputAmount = 5 ether; string memory symbol = "WETH"; IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); - vm.label(address(inputToken), "WETH"); - IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - vm.label(address(outputToken), "USDC"); + deployCodeTo("MockGateway.sol", address(gateway)); + MockGateway mockGateway = MockGateway(address(gateway)); + mockGateway.saveTokenAddress(symbol, address(inputToken)); deal(address(inputToken), address(this), inputAmount); inputToken.transfer(address(handler), inputAmount); - assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + bytes memory payload = + hex"000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000ef211076b8d8b46797e09c9a374fb4cdc1df09160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"; - assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); - assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + ); + } + + function _mockGateway(string memory symbol, address tokenIn, uint256 amountIn) internal { + deal(tokenIn, address(handler), amountIn); + + if (isForked) { + deployCodeTo("MockGateway.sol", address(gateway)); + } - deployCodeTo("MockGateway.sol", address(gateway)); MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, address(inputToken)); + mockGateway.saveTokenAddress(symbol, tokenIn); + mockGateway.saveTokenAddress("WETH", WETH); + } + + function _build_exactInputSingle(address tokenOut, uint256 amountOutMin, uint24 fee) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(0), tokenOut, amountOutMin, abi.encode(uint24(fee), uint160(0))); + } + + function _build_exactInput(address tokenOut, uint256 amountOutMin, bytes memory path) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(1), tokenOut, amountOutMin, path); + } + + function _build_exactTokensForTokens(address tokenOut, uint256 amountOut, address[] memory path) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(2), tokenOut, amountOut, abi.encode(path)); + } + + function _build_exactOutputSingle(address tokenOut, uint256 amountOut, uint24 fee) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(3), tokenOut, amountOut, abi.encode(uint24(fee), uint160(0))); + } + + function _build_exactOutput(address tokenOut, uint256 amountOut, bytes memory path) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(4), tokenOut, amountOut, path); + } + + function _build_tokensForExactTokens(address tokenOut, uint256 amountOut, address[] memory path) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(5), tokenOut, amountOut, abi.encode(path)); + } + + function _build_executeWithToken(address destination, bool unwrap, bytes memory swapPayload) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(2), abi.encode(destination, unwrap, swapPayload)); + } + + function _build_executeWithToken_multiswap(address destination, bool unwrap, bytes[] memory swapsPayloads) + internal + pure + returns (bytes memory) + { + return abi.encode(uint8(3), abi.encode(destination, unwrap, swapsPayloads)); + } + + function _execute_exactInputSingleSwap( + string memory symbol, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMin, + address destination, + bool unwrap + ) internal { + _mockGateway(symbol, tokenIn, amountIn); + + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); + + _assertPreSwap(inputToken, outputToken, amountIn, destination); - bytes memory swapPayload = - abi.encode(uint8(3), address(outputToken), amountOut, abi.encode(uint24(3000), uint160(0))); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); + bytes memory swapPayload = _build_exactInputSingle(tokenOut, amountOutMin, 3000); + bytes memory payload = _build_executeWithToken(destination, unwrap, swapPayload); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn ); - assertGt(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); - assertEq(outputToken.balanceOf(ALICE), amountOut, "Alice output token balance after"); - assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); + _assertPostSwapExactInput(inputToken, outputToken, amountOutMin, destination, unwrap); } - function test_executeWithToken_exactOutputSwap() public forked { - uint256 inputAmount = 1 ether; - uint256 amountOut = 1000 * 1e6; // 1000 USDC + function _execute_exactInputSwap( + string memory symbol, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMin, + address destination, + bool unwrap, + bytes memory path + ) internal { + _mockGateway(symbol, tokenIn, amountIn); - string memory symbol = "WETH"; - IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); - vm.label(address(inputToken), "WETH"); + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); - IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - vm.label(address(outputToken), "USDC"); + _assertPreSwap(inputToken, outputToken, amountIn, destination); - deal(address(inputToken), address(this), inputAmount); - inputToken.transfer(address(handler), inputAmount); + bytes memory swapPayload = _build_exactInput(tokenOut, amountOutMin, path); + bytes memory payload = _build_executeWithToken(destination, unwrap, swapPayload); - assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn + ); + + _assertPostSwapExactInput(inputToken, outputToken, amountOutMin, destination, unwrap); + } - assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); - assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + function _execute_exactTokensForTokens( + string memory symbol, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMin, + address destination, + bool unwrap, + address[] memory path + ) internal { + _mockGateway(symbol, tokenIn, amountIn); - deployCodeTo("MockGateway.sol", address(gateway)); - MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, address(inputToken)); + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); + + _assertPreSwap(inputToken, outputToken, amountIn, destination); - bytes memory path = abi.encodePacked(address(outputToken), uint24(500), address(inputToken)); - bytes memory swapPayload = abi.encode(uint8(4), address(outputToken), amountOut, path); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); + bytes memory swapPayload = _build_exactTokensForTokens(tokenOut, amountOutMin, path); + bytes memory payload = _build_executeWithToken(destination, unwrap, swapPayload); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn ); - assertGt(inputToken.balanceOf(ALICE), 0, "Alice input token balance after"); - assertEq(outputToken.balanceOf(ALICE), amountOut, "Alice output token balance after"); - assertEq(inputToken.balanceOf(address(handler)), 0, "Handler input token balance after"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance after"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router allowance after swap"); + _assertPostSwapExactInput(inputToken, outputToken, amountOutMin, destination, unwrap); } - function test_executeWithToken_tokensForExactTokensSwap() public forked { - uint256 inputAmount = 5 ether; - uint256 amountOut = 10_000 * 1e6; + function _execute_exactOutputSingleSwap( + string memory symbol, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOut, + address destination, + bool unwrap + ) internal { + _mockGateway(symbol, tokenIn, amountIn); - string memory symbol = "WETH"; - IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); - vm.label(address(inputToken), "WETH"); + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); - IERC20Upgradeable outputToken = IERC20Upgradeable(USDC); - vm.label(address(inputToken), "USDC"); + _assertPreSwap(inputToken, outputToken, amountIn, destination); - deal(address(inputToken), address(this), inputAmount); - inputToken.transfer(address(handler), inputAmount); + bytes memory swapPayload = _build_exactOutputSingle(tokenOut, amountOut, 3000); + bytes memory payload = _build_executeWithToken(destination, unwrap, swapPayload); - assertEq(inputToken.balanceOf(address(handler)), inputAmount, "Handler input token balance before"); - assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + handler.executeWithToken( + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn + ); - assertEq(inputToken.balanceOf(ALICE), 0, "Alice input token balance before"); - assertEq(outputToken.balanceOf(ALICE), 0, "Alice output token balance before"); + _assertPostSwapExactOutput(inputToken, outputToken, amountOut, destination, unwrap); + } - deployCodeTo("MockGateway.sol", address(gateway)); - MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, address(inputToken)); + function _execute_exactOutputSwap( + string memory symbol, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOut, + address destination, + bool unwrap, + bytes memory path + ) internal { + _mockGateway(symbol, tokenIn, amountIn); - address[] memory path = new address[](2); - path[0] = address(inputToken); - path[1] = address(outputToken); + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); + + _assertPreSwap(inputToken, outputToken, amountIn, destination); - bytes memory swapPayload = abi.encode(uint8(5), address(outputToken), amountOut, abi.encode(path)); - bytes memory payload = abi.encode(uint8(2), abi.encode(ALICE, false, swapPayload)); + bytes memory swapPayload = _build_exactOutput(tokenOut, amountOut, path); + bytes memory payload = _build_executeWithToken(destination, unwrap, swapPayload); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn ); - assertGt(inputToken.balanceOf(ALICE), 0, "User got refunded input"); - assertEq(outputToken.balanceOf(ALICE), amountOut, "User balance didn't increase"); - assertEq(inputToken.balanceOf(address(handler)), 0, "Dust leftover in the contract."); - assertEq(outputToken.balanceOf(address(handler)), 0, "Funds leftover in contract"); - assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); + _assertPostSwapExactOutput(inputToken, outputToken, amountOut, destination, unwrap); } - function test_executeWithToken_custom() public forked { - uint256 inputAmount = 5 ether; - string memory symbol = "WETH"; - IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses(symbol)); + function _execute_tokensForExactTokens( + string memory symbol, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOut, + address destination, + bool unwrap, + address[] memory path + ) internal { + _mockGateway(symbol, tokenIn, amountIn); - deployCodeTo("MockGateway.sol", address(gateway)); - MockGateway mockGateway = MockGateway(address(gateway)); - mockGateway.saveTokenAddress(symbol, address(inputToken)); + IERC20 inputToken = IERC20(tokenIn); + IERC20 outputToken = IERC20(tokenOut); - deal(address(inputToken), address(this), inputAmount); - inputToken.transfer(address(handler), inputAmount); + _assertPreSwap(inputToken, outputToken, amountIn, destination); - bytes memory payload = - hex"000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000ef211076b8d8b46797e09c9a374fb4cdc1df09160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"; + bytes memory swapPayload = _build_tokensForExactTokens(tokenOut, amountOut, path); + bytes memory payload = _build_executeWithToken(destination, unwrap, swapPayload); handler.executeWithToken( - keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, inputAmount + keccak256(abi.encodePacked("COMMAND_ID")), "osmosis-7", "mock_address", payload, symbol, amountIn ); + + _assertPostSwapExactOutput(inputToken, outputToken, amountOut, destination, unwrap); + } + + function _assertPreSwap(IERC20 inputToken, IERC20 outputToken, uint256 amountIn, address destination) internal { + assertEq(inputToken.balanceOf(address(handler)), amountIn, "Handler input token balance before"); + assertEq(outputToken.balanceOf(address(handler)), 0, "Handler output token balance before"); + + assertEq(destination.balance, 0, "Destination eth balance before"); + assertEq(inputToken.balanceOf(destination), 0, "Destination input token balance before"); + assertEq(outputToken.balanceOf(destination), 0, "Destination output token balance before"); + } + + function _assertPostSwapExactInput( + IERC20 inputToken, + IERC20 outputToken, + uint256 amountOutMin, + address destination, + bool unwrap + ) internal { + assertEq(inputToken.balanceOf(destination), 0, "Destination input token balance after"); + if (unwrap) { + assertTrue(destination.balance >= amountOutMin, "Destination output native balance after"); + _assertNoDust(); + } else { + assertTrue(outputToken.balanceOf(destination) >= amountOutMin, "Destination output token balance after"); + } + + _assertNoDust(inputToken); + _assertNoDust(outputToken); + _assertNoAllowance(inputToken); + } + + function _assertPostSwapExactOutput( + IERC20 inputToken, + IERC20 outputToken, + uint256 amountOut, + address destination, + bool unwrap + ) internal { + assertGt(inputToken.balanceOf(destination), 0, "Destination input token balance after"); + console2.log("Input refund: ", inputToken.balanceOf(destination)); + + if (unwrap) { + assertEq(destination.balance, amountOut, "Destination output native balance after"); + _assertNoDust(); + } else { + assertEq(outputToken.balanceOf(destination), amountOut, "Destination output token balance after"); + } + + _assertNoDust(inputToken); + _assertNoDust(outputToken); + _assertNoAllowance(inputToken); + } + + function _assertNoDust(IERC20[] memory tokens) internal { + uint256 length = tokens.length; + for (uint256 i; i < length; ++i) { + _assertNoDust(tokens[i]); + } + } + + function _assertNoDust(IERC20 token) internal { + assertEq(token.balanceOf(address(handler)), 0, "Dust left in handler"); + } + + function _assertNoDust() internal { + assertEq(address(handler).balance, 0, "Handler native token balance after"); + } + + function _assertNoAllowance(IERC20[] memory tokens) internal { + uint256 length = tokens.length; + for (uint256 i; i < length; ++i) { + _assertNoAllowance(tokens[i]); + } + } + + function _assertNoAllowance(IERC20 token) internal { + assertEq(token.allowance(address(handler), address(router)), 0, "Allowance left in handler"); } - // function test_executeWithToken_swap_refundDust() public { + // function test_forked_executeWithToken_swap_refundDust() public { // uint256 inputAmount = 4.95 ether; // uint256 dust = 0.05 ether; // uint256 minOutput = 10_000 * 1e6; @@ -691,12 +992,7 @@ contract AxelarHandlerTest is Test { // assertEq(inputToken.allowance(address(handler), address(router)), 0, "Router Allowance Remaining After Payment"); // } - function test_swapAndGmpTransferERC20Token_ETH( - uint32 domain, - address inputToken, - uint256 inputAmount, - bytes memory swapCalldata - ) public forked { + function test_forked_swapAndGmpTransferERC20Token_ETH() public forked { uint256 amount = 2 ether; uint256 gasAmount = 0.5 ether; IERC20Upgradeable token = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses("WBTC")); @@ -732,12 +1028,7 @@ contract AxelarHandlerTest is Test { assertEq(address(handler).balance, 0, "Native balance after sending."); } - function test_swapAndGmpTransferERC20Token_Token( - uint32 domain, - address inputToken, - uint256 inputAmount, - bytes memory swapCalldata - ) public forked { + function test_forked_swapAndGmpTransferERC20Token_Token() public forked { uint256 amount = 2 ether; uint256 gasAmount = 0.5 ether; IERC20Upgradeable inputToken = IERC20Upgradeable(IAxelarGateway(gateway).tokenAddresses("WETH")); From b96a14eab875c717cc43df3a6452df8542fb0650 Mon Sep 17 00:00:00 2001 From: Manu Date: Tue, 27 Aug 2024 17:17:00 +0200 Subject: [PATCH 15/17] Fix EVM Version to Paris to allow compatibility with Fantom --- AxelarHandler/foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/AxelarHandler/foundry.toml b/AxelarHandler/foundry.toml index 3dad954..c0c4557 100644 --- a/AxelarHandler/foundry.toml +++ b/AxelarHandler/foundry.toml @@ -2,6 +2,7 @@ src = "src" out = "out" libs = ["lib"] +evm_version = "paris" # See more config options https://github.com/foundry-rs/foundry/tree/master/config [rpc_endpoints] From 7af6da660ee5100c9c944498dcf86861a5e3c37a Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 6 Sep 2024 01:20:44 +0200 Subject: [PATCH 16/17] Switch to try/catch for unwrapping --- AxelarHandler/src/AxelarHandler.sol | 23 ++++++------------- .../src/libraries/SkipSwapRouter.sol | 15 ++++++++++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index 956efc4..55a24db 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -382,18 +382,15 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, _sendToken(token, amount, destination); } else if (command == Commands.SendNative) { address destination = abi.decode(data, (address)); - if (_wETHSymbolHash != DISABLED_SYMBOL && keccak256(abi.encodePacked(tokenSymbol)) == _wETHSymbolHash) { - _sendNative(token, amount, destination); - } else { - _sendToken(token, amount, destination); - } + + _sendNative(token, amount, destination); } else if (command == Commands.Swap) { (address destination, bool unwrapOut, bytes memory swap) = abi.decode(data, (address, bool, bytes)); try SkipSwapRouter.swap(swapRouter, destination, tokenIn, amount, swap) returns ( IERC20 tokenOut, uint256 amountOut ) { - if (unwrapOut && address(tokenOut) == _getTokenAddress(wETHSymbol)) { + if(unwrapOut) { _sendNative(address(tokenOut), amountOut, destination); } else { _sendToken(address(tokenOut), amountOut, destination); @@ -408,7 +405,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, try SkipSwapRouter.multiSwap(swapRouter, destination, tokenIn, amount, swaps) returns ( IERC20 tokenOut, uint256 amountOut ) { - if (unwrapOut && address(tokenOut) == _getTokenAddress(wETHSymbol)) { + if(unwrapOut) { _sendNative(address(tokenOut), amountOut, destination); } else { _sendToken(address(tokenOut), amountOut, destination); @@ -426,15 +423,9 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, } function _sendNative(address token, uint256 amount, address destination) internal { - // Unwrap native token. - IWETH weth = IWETH(token); - weth.withdraw(amount); - - // Send it unwrapped to the destination - (bool success,) = destination.call{value: amount}(""); - - if (!success) { - revert NativePaymentFailed(); + try SkipSwapRouter.sendNative(token, amount, destination) {} + catch { + _sendToken(token, amount, destination); } } diff --git a/AxelarHandler/src/libraries/SkipSwapRouter.sol b/AxelarHandler/src/libraries/SkipSwapRouter.sol index d7becdc..d208ce2 100644 --- a/AxelarHandler/src/libraries/SkipSwapRouter.sol +++ b/AxelarHandler/src/libraries/SkipSwapRouter.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IWETH} from "../interfaces/IWETH.sol"; import {ISwapRouter02} from "../interfaces/ISwapRouter02.sol"; import {BytesLib, Path} from "./Path.sol"; @@ -13,6 +14,7 @@ library SkipSwapRouter { using Path for bytes; error InsufficientOutputAmount(); + error NativePaymentFailed(); enum SwapCommands { ExactInputSingle, @@ -116,6 +118,19 @@ library SkipSwapRouter { } } + function sendNative(address token, uint256 amount, address destination) external { + // Unwrap native token. + IWETH weth = IWETH(token); + weth.withdraw(amount); + + // Send it unwrapped to the destination + (bool success,) = destination.call{value: amount}(""); + + if (!success) { + revert NativePaymentFailed(); + } + } + function _fixPath(address tokenA, address tokenB, bytes memory path) internal pure returns (bytes memory) { (address decodedA,,) = path.decodeFirstPool(); if (decodedA != tokenA) { From 701cca000dc4b0aaa1199d7f34624cc7b7190d23 Mon Sep 17 00:00:00 2001 From: thal0x Date: Fri, 6 Sep 2024 05:55:29 -0500 Subject: [PATCH 17/17] remove console from contracts --- AxelarHandler/src/AxelarHandler.sol | 7 ++----- AxelarHandler/src/libraries/SkipSwapRouter.sol | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/AxelarHandler/src/AxelarHandler.sol b/AxelarHandler/src/AxelarHandler.sol index 55a24db..b36845a 100644 --- a/AxelarHandler/src/AxelarHandler.sol +++ b/AxelarHandler/src/AxelarHandler.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.18; -import {console} from "forge-std/console.sol"; - import {IWETH} from "./interfaces/IWETH.sol"; import {IAxelarGasService} from "lib/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol"; @@ -390,7 +388,7 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, try SkipSwapRouter.swap(swapRouter, destination, tokenIn, amount, swap) returns ( IERC20 tokenOut, uint256 amountOut ) { - if(unwrapOut) { + if (unwrapOut) { _sendNative(address(tokenOut), amountOut, destination); } else { _sendToken(address(tokenOut), amountOut, destination); @@ -400,12 +398,11 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable, } } else if (command == Commands.MultiSwap) { (address destination, bool unwrapOut, bytes[] memory swaps) = abi.decode(data, (address, bool, bytes[])); - console.log("debug"); try SkipSwapRouter.multiSwap(swapRouter, destination, tokenIn, amount, swaps) returns ( IERC20 tokenOut, uint256 amountOut ) { - if(unwrapOut) { + if (unwrapOut) { _sendNative(address(tokenOut), amountOut, destination); } else { _sendToken(address(tokenOut), amountOut, destination); diff --git a/AxelarHandler/src/libraries/SkipSwapRouter.sol b/AxelarHandler/src/libraries/SkipSwapRouter.sol index d208ce2..02ca8b4 100644 --- a/AxelarHandler/src/libraries/SkipSwapRouter.sol +++ b/AxelarHandler/src/libraries/SkipSwapRouter.sol @@ -5,8 +5,6 @@ import {IWETH} from "../interfaces/IWETH.sol"; import {ISwapRouter02} from "../interfaces/ISwapRouter02.sol"; import {BytesLib, Path} from "./Path.sol"; -import {console} from "forge-std/Test.sol"; - pragma solidity >= 0.8.18; library SkipSwapRouter {