Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API-3529] Implement GoFastHandler contract #15

Merged
merged 10 commits into from
Nov 13, 2024
7 changes: 7 additions & 0 deletions AxelarHandler/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
lib/axelar-gmp-sdk-solidity/=lib/axelar-gmp-sdk-solidity/
lib/ds-test/=lib/forge-std/lib/ds-test/src/
lib/erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/
lib/forge-std/=lib/forge-std
lib/openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/
lib/openzeppelin-contracts/=lib/openzeppelin-contracts/
lib/openzeppelin/=lib/openzeppelin-contracts-upgradeable/contracts/
2 changes: 1 addition & 1 deletion AxelarHandler/src/AxelarExecutableUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,4 @@ contract AxelarExecutableUpgradeable is IAxelarExecutable, Initializable {
string calldata tokenSymbol,
uint256 amount
) internal virtual {}
}
}
2 changes: 1 addition & 1 deletion AxelarHandler/src/AxelarHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -427,4 +427,4 @@ contract AxelarHandler is AxelarExecutableUpgradeable, Ownable2StepUpgradeable,
}

function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
}
73 changes: 73 additions & 0 deletions AxelarHandler/src/GoFastHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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 {IFastTransferGateway} from "./interfaces/IFastTransferGateway.sol";

contract GoFastHandler {
dhfang marked this conversation as resolved.
Show resolved Hide resolved
using SafeERC20 for IERC20;

ISwapRouter02 public swapRouter;
IFastTransferGateway public fastTransferGateway;
dhfang marked this conversation as resolved.
Show resolved Hide resolved

constructor(address _swapRouter, address _fastTransferGateway) {
swapRouter = ISwapRouter02(_swapRouter);
fastTransferGateway = IFastTransferGateway(_fastTransferGateway);
}

function swapAndSubmitOrder(
address tokenIn,
uint256 swapAmountIn,
bytes memory swapCalldata,
uint256 feeAmount,
dhfang marked this conversation as resolved.
Show resolved Hide resolved
bytes32 sender,
bytes32 recipient,
uint32 destinationDomain,
uint64 timeoutTimestamp,
bytes calldata destinationCalldata
) public payable returns (bytes32) {
require(feeAmount != 0, "fast transfer fee cannot be zero");

uint256 swapAmountOut = _swap(tokenIn, swapAmountIn, swapCalldata);

require(swapAmountOut >= feeAmount, "amount received from swap is less than fast transfer fee");

// this is the amount that the recipient will receive on the destination chain
uint256 swapAmountOutAfterFee = swapAmountOut - feeAmount;

return fastTransferGateway.submitOrder(
sender,
recipient,
swapAmountOut,
swapAmountOutAfterFee,
destinationDomain,
timeoutTimestamp,
destinationCalldata
);
}

function _swap(address tokenIn, uint256 amountIn, bytes memory swapCalldata) internal returns (uint256 amountOut) {
address tokenOut = fastTransferGateway.token();
Copy link
Collaborator

@dhfang dhfang Nov 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its a little strange that the caller needs to match their swapCallData to swap into a token whose address is stored in the gateway. This is probably fine in practice since we are generating the swap payload, but if we mess up, can we run into situations where funds are stuck if the swap output token in the swapCallData does not match the gateway token?


uint256 tokenOutBalanceBefore = IERC20(tokenOut).balanceOf(address(this));

if (tokenIn != address(0)) {
dhfang marked this conversation as resolved.
Show resolved Hide resolved
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);

IERC20(tokenIn).safeApprove(address(swapRouter), amountIn);
}

(bool success,) = address(swapRouter).call{value: msg.value}(swapCalldata);
dhfang marked this conversation as resolved.
Show resolved Hide resolved
if (!success) {
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}

amountOut = IERC20(tokenOut).balanceOf(address(this)) - tokenOutBalanceBefore;
}
}
16 changes: 16 additions & 0 deletions AxelarHandler/src/interfaces/IFastTransferGateway.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

interface IFastTransferGateway {
function submitOrder(
bytes32 sender,
bytes32 recipient,
uint256 amountIn,
uint256 amountOut,
uint32 destinationDomain,
uint64 timeoutTimestamp,
bytes calldata data
) external returns (bytes32);

function token() external view returns (address);
}
249 changes: 249 additions & 0 deletions AxelarHandler/test/GoFastHandler.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Test.sol";

import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

import {GoFastHandler} from "../src/GoFastHandler.sol";
import {IFastTransferGateway} from "../src/interfaces/IFastTransferGateway.sol";

contract GoFastHandlerTest is Test {
uint256 arbitrumFork;

address fastTransferGateway;
address uniswapRouter;
address usdc = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;

GoFastHandler handler;

address alice;

function setUp() public {
arbitrumFork = vm.createFork(vm.envString("RPC_URL"));

vm.selectFork(arbitrumFork);
vm.rollFork(242534997);

fastTransferGateway = address(0xC);
uniswapRouter = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;

handler = new GoFastHandler(uniswapRouter, fastTransferGateway);

alice = makeAddr("alice");
}

function testSwapAndSubmitOrderERC20() public {
address tokenIn = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; // WETH
uint256 amountIn = 1 ether;
uint256 fastTransferFee = 1_000_000; // 1 USDC
uint32 destinationDomain = 10;
uint64 timeoutTimestamp = uint64(block.timestamp + 100);
bytes32 sender = keccak256("sender");
bytes32 recipient = keccak256("recipient");

bytes memory swapCalldata = _encodeSwapExactInputCalldata(tokenIn, usdc, 500, address(handler), amountIn, 0, 0);

deal(tokenIn, alice, amountIn);

vm.mockCall(fastTransferGateway, abi.encodeWithSelector(IFastTransferGateway.token.selector), abi.encode(usdc));

vm.mockCall(
fastTransferGateway,
abi.encodeWithSelector(
IFastTransferGateway.submitOrder.selector,
sender,
recipient,
2702776834,
2701776834,
destinationDomain,
timeoutTimestamp,
""
),
abi.encode(keccak256("orderId"))
);

vm.startPrank(alice);
IERC20(tokenIn).approve(address(handler), amountIn);

bytes32 orderId = handler.swapAndSubmitOrder(
tokenIn, amountIn, swapCalldata, fastTransferFee, sender, recipient, destinationDomain, timeoutTimestamp, ""
);
vm.stopPrank();

assertEq(orderId, keccak256("orderId"));
}

function testSwapAndSubmitOrderUSDT() public {
address tokenIn = 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9; // USDT
uint256 amountIn = 100_000_000;
uint256 fastTransferFee = 1_000_000; // 1 USDC
uint32 destinationDomain = 10;
uint64 timeoutTimestamp = uint64(block.timestamp + 100);
bytes32 sender = keccak256("sender");
bytes32 recipient = keccak256("recipient");

bytes memory swapCalldata = _encodeSwapExactInputCalldata(tokenIn, usdc, 500, address(handler), amountIn, 0, 0);

deal(tokenIn, alice, amountIn);

vm.mockCall(fastTransferGateway, abi.encodeWithSelector(IFastTransferGateway.token.selector), abi.encode(usdc));

vm.mockCall(
fastTransferGateway,
abi.encodeWithSelector(
IFastTransferGateway.submitOrder.selector,
sender,
recipient,
99963678,
98963678,
destinationDomain,
timeoutTimestamp,
""
),
abi.encode(keccak256("orderId"))
);

vm.startPrank(alice);
IERC20(tokenIn).approve(address(handler), amountIn);

bytes32 orderId = handler.swapAndSubmitOrder(
tokenIn, amountIn, swapCalldata, fastTransferFee, sender, recipient, destinationDomain, timeoutTimestamp, ""
);
vm.stopPrank();

assertEq(orderId, keccak256("orderId"));
}

function testSwapAndSubmitOrderNative() public {
address tokenIn = address(0); // ETH
uint256 amountIn = 1 ether;
uint256 fastTransferFee = 1_000_000; // 1 USDC
uint32 destinationDomain = 10;
uint64 timeoutTimestamp = uint64(block.timestamp + 100);
bytes32 sender = keccak256("sender");
bytes32 recipient = keccak256("recipient");

bytes memory swapCalldata = _encodeSwapExactInputCalldata(
0x82aF49447D8a07e3bd95BD0d56f35241523fBab1, usdc, 500, address(handler), amountIn, 0, 0
);

deal(alice, amountIn);

vm.mockCall(fastTransferGateway, abi.encodeWithSelector(IFastTransferGateway.token.selector), abi.encode(usdc));

vm.mockCall(
fastTransferGateway,
abi.encodeWithSelector(
IFastTransferGateway.submitOrder.selector,
sender,
recipient,
2702776834,
2701776834,
destinationDomain,
timeoutTimestamp,
""
),
abi.encode(keccak256("orderId"))
);

vm.startPrank(alice);
bytes32 orderId = handler.swapAndSubmitOrder{value: amountIn}(
tokenIn, amountIn, swapCalldata, fastTransferFee, sender, recipient, destinationDomain, timeoutTimestamp, ""
);
vm.stopPrank();

assertEq(orderId, keccak256("orderId"));
}

function testSwapAndSubmitOrderRevertsIfSwapFails() public {
address tokenIn = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; // WETH
uint256 amountIn = 1 ether;
uint256 amountOutMinimum = 1_000_000_000_000; // 1,000,000 USDC
uint256 fastTransferFee = 1_000_000; // 1 USDC
uint32 destinationDomain = 10;
uint64 timeoutTimestamp = uint64(block.timestamp + 100);
bytes32 sender = keccak256("sender");
bytes32 recipient = keccak256("recipient");

bytes memory swapCalldata =
_encodeSwapExactInputCalldata(tokenIn, usdc, 500, address(handler), amountIn, amountOutMinimum, 0);

deal(tokenIn, alice, amountIn);

vm.mockCall(fastTransferGateway, abi.encodeWithSelector(IFastTransferGateway.token.selector), abi.encode(usdc));

vm.startPrank(alice);
IERC20(tokenIn).approve(address(handler), amountIn);

vm.expectRevert("Too little received");
handler.swapAndSubmitOrder(
tokenIn, amountIn, swapCalldata, fastTransferFee, sender, recipient, destinationDomain, timeoutTimestamp, ""
);
vm.stopPrank();
}

function testSwapAndSubmitOrderRevertsIfSwapAmountOutIsLessThanFastTransferFee() public {
address tokenIn = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; // WETH
uint256 amountIn = 1 ether;
uint256 fastTransferFee = 3000_000_000; // 3000 USDC
uint32 destinationDomain = 10;
uint64 timeoutTimestamp = uint64(block.timestamp + 100);
bytes32 sender = keccak256("sender");
bytes32 recipient = keccak256("recipient");

bytes memory swapCalldata = _encodeSwapExactInputCalldata(tokenIn, usdc, 500, address(handler), amountIn, 0, 0);

deal(tokenIn, alice, amountIn);

vm.mockCall(fastTransferGateway, abi.encodeWithSelector(IFastTransferGateway.token.selector), abi.encode(usdc));

vm.startPrank(alice);
IERC20(tokenIn).approve(address(handler), amountIn);

vm.expectRevert("amount received from swap is less than fast transfer fee");
handler.swapAndSubmitOrder(
tokenIn, amountIn, swapCalldata, fastTransferFee, sender, recipient, destinationDomain, timeoutTimestamp, ""
);
vm.stopPrank();
}

function testSwapAndSubmitOrderRevertsIfFeeAmountIsZero() public {
address tokenIn = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; // WETH
uint256 amountIn = 1 ether;
uint256 fastTransferFee = 0;
uint32 destinationDomain = 10;
uint64 timeoutTimestamp = uint64(block.timestamp + 100);
bytes32 sender = keccak256("sender");
bytes32 recipient = keccak256("recipient");

bytes memory swapCalldata = _encodeSwapExactInputCalldata(tokenIn, usdc, 500, address(handler), amountIn, 0, 0);

deal(tokenIn, alice, amountIn);

vm.mockCall(fastTransferGateway, abi.encodeWithSelector(IFastTransferGateway.token.selector), abi.encode(usdc));

vm.startPrank(alice);
IERC20(tokenIn).approve(address(handler), amountIn);

vm.expectRevert("fast transfer fee cannot be zero");
handler.swapAndSubmitOrder(
tokenIn, amountIn, swapCalldata, fastTransferFee, sender, recipient, destinationDomain, timeoutTimestamp, ""
);
vm.stopPrank();
}

function _encodeSwapExactInputCalldata(
address tokenIn,
address tokenOut,
uint24 fee,
address recipient,
uint256 amountIn,
uint256 amountOutMinimum,
uint160 sqrtPriceLimitX96
) internal pure returns (bytes memory) {
return abi.encodeWithSelector(
bytes4(0x04e45aaf), tokenIn, tokenOut, fee, recipient, amountIn, amountOutMinimum, sqrtPriceLimitX96
);
}
}
Loading