diff --git a/.gitmodules b/.gitmodules index 6e8936c..cab0914 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,3 +12,6 @@ path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts branch = v4.9.0 +[submodule "lib/interfaces"] + path = lib/interfaces + url = https://github.com/connext/interfaces diff --git a/lib/interfaces b/lib/interfaces new file mode 160000 index 0000000..2e02873 --- /dev/null +++ b/lib/interfaces @@ -0,0 +1 @@ +Subproject commit 2e0287382f95ce64abd7f5257e0a17a84795f370 diff --git a/remappings.txt b/remappings.txt index e3a19ae..3113e61 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,5 @@ axelar-gmp-sdk-solidity/=lib/axelar-gmp-sdk-solidity/ ds-test/=lib/forge-std/lib/ds-test/src/ forge-gas-snapshot/=lib/forge-gas-snapshot/src/ forge-std/=lib/forge-std/src/ -openzeppelin-contracts/=lib/openzeppelin-contracts/ \ No newline at end of file +openzeppelin-contracts/=lib/openzeppelin-contracts/ +connext-interfaces/=lib/interfaces/ \ No newline at end of file diff --git a/src/adapters/ConnextAdapter.sol b/src/adapters/ConnextAdapter.sol new file mode 100644 index 0000000..866db23 --- /dev/null +++ b/src/adapters/ConnextAdapter.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.10; + +import "../interfaces/ISushiXSwapV2Adapter.sol"; +import "../interfaces/IRouteProcessor.sol"; +import "../interfaces/IWETH.sol"; + +import {IXReceiver} from "connext-interfaces/core/IXReceiver.sol"; +import {IConnext} from "connext-interfaces/core/IConnext.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + + + +contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { + using SafeERC20 for IERC20; + + IConnext public immutable connext; + IRouteProcessor public immutable rp; + IWETH public immutable weth; + + address constant NATIVE_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + struct ConnextBridgeParams { + uint32 destinationDomain; // connext dst chain id + address target; // destination address for _execute call + address to; // address for fallback transfers on _execute call + address token; // token getting bridged + uint256 amount; // amount to bridge + uint256 slippage; // max amount of slippage willing to take in BPS (e.g. 30 = 0.3%) + } + + error RpSentNativeIn(); + error NotConnext(); + error NoGasReceived(); + + constructor( + address _connext, + address _rp, + address _weth + ) { + connext = IConnext(_connext); + rp = IRouteProcessor(_rp); + weth = IWETH(_weth); + } + + /// @inheritdoc ISushiXSwapV2Adapter + function swap( + uint256 _amountBridged, + bytes calldata _swapData, + address _token, + bytes calldata _payloadData + ) external payable override { + IRouteProcessor.RouteProcessorData memory rpd = abi.decode( + _swapData, + (IRouteProcessor.RouteProcessorData) + ); + + // send tokens to RP + IERC20(rpd.tokenIn).safeTransfer(address(rp), _amountBridged); + + rp.processRoute( + rpd.tokenIn, + _amountBridged, + rpd.tokenOut, + rpd.amountOutMin, + rpd.to, + rpd.route + ); + + // tokens should be sent via rp + if (_payloadData.length > 0) { + PayloadData memory pd = abi.decode(_payloadData, (PayloadData)); + try + IPayloadExecutor(pd.target).onPayloadReceive{gas: pd.gasLimit}( + pd.targetData + ) + {} catch (bytes memory) { + revert(); + } + } + } + + /// @inheritdoc ISushiXSwapV2Adapter + function executePayload( + uint256 _amountBridged, + bytes calldata _payloadData, + address _token + ) external payable override { + PayloadData memory pd = abi.decode(_payloadData, (PayloadData)); + IERC20(_token).safeTransfer(pd.target, _amountBridged); + IPayloadExecutor(pd.target).onPayloadReceive{gas: pd.gasLimit}( + pd.targetData + ); + } + + // todo: getFee - think there is a way to fetch this on-chain + + + /// @inheritdoc ISushiXSwapV2Adapter + function adapterBridge( + bytes calldata _adapterData, + address _refundAddress, + bytes calldata _swapData, + bytes calldata _payloadData + ) external payable override { + ConnextBridgeParams memory params = abi.decode( + _adapterData, + (ConnextBridgeParams) + ); + + if (params.token == NATIVE_ADDRESS) { + // RP should not send native in, since we won't know the exact amount to bridge + if (params.amount == 0) revert RpSentNativeIn(); + + weth.deposit{value: params.amount}(); + params.token = address(weth); + } + + if (params.amount == 0) + params.amount = IERC20(params.token).balanceOf(address(this)); + + IERC20(params.token).forceApprove( + address(connext), + params.amount + ); + + // build payload from params.to, _swapData, and _payloadData + bytes memory payload = abi.encode(params.to, _swapData, _payloadData); + + // check if gas was received, since it doesn't throw on xcall + if (address(this).balance == 0) + revert NoGasReceived(); + + connext.xcall{value: address(this).balance} ( + params.destinationDomain, + params.target, + params.token, + _refundAddress, + params.amount, + params.slippage, + payload + ); + } + + /// @notice receiver function on dst chain + /// @param _transferId id of the xchain transaction + /// @param _amount amount of tokeks that were bridged + /// @param _asset asset that was bridged + /// @param _originSender address of the sender on the origin chain + /// @param _origin chain id of the origin chain + /// @param _callData data received from source chain + function xReceive( + bytes32 _transferId, + uint256 _amount, + address _asset, + address _originSender, + uint32 _origin, + bytes memory _callData + ) external override returns (bytes memory) { + uint256 gasLeft = gasleft(); + if (msg.sender != address(connext)) + revert NotConnext(); + + (address to, bytes memory _swapData, bytes memory _payloadData) = abi + .decode(_callData, (address, bytes, bytes)); + + uint256 reserveGas = 100000; + + if (gasLeft < reserveGas) { + IERC20(_asset).safeTransfer(to, _amount); + + /// @dev transfer any native token + if (address(this).balance > 0) + to.call{value: (address(this).balance)}(""); + + return bytes(""); + } + + // 100000 -> exit gas + uint256 limit = gasLeft - reserveGas; + + if (_swapData.length > 0) { + try + ISushiXSwapV2Adapter(address(this)).swap{gas: limit}( + _amount, + _swapData, + _asset, + _payloadData + ) + {} catch (bytes memory) {} + } else if (_payloadData.length > 0) { + try + ISushiXSwapV2Adapter(address(this)).executePayload{gas: limit}( + _amount, + _payloadData, + _asset + ) + {} catch (bytes memory) {} + } + + if (IERC20(_asset).balanceOf(address(this)) > 0) + IERC20(_asset).safeTransfer(to, IERC20(_asset).balanceOf(address(this))); + + /// @dev transfer any native token received as dust to the to address + if (address(this).balance > 0) + to.call{value: (address(this).balance)}(""); + } + + /// @inheritdoc ISushiXSwapV2Adapter + function sendMessage(bytes calldata _adapterData) external override { + (_adapterData); + revert(); + } + + receive() external payable {} +} \ No newline at end of file diff --git a/test/AxelarAdapterTests/AxelarAdapterBridgeTest.t.sol b/test/AxelarAdapterTests/AxelarAdapterBridgeTest.t.sol index 8c9fa85..9f8d417 100644 --- a/test/AxelarAdapterTests/AxelarAdapterBridgeTest.t.sol +++ b/test/AxelarAdapterTests/AxelarAdapterBridgeTest.t.sol @@ -190,9 +190,9 @@ contract AxelarAdapterBridgeTest is BaseTest { assertEq( address(axelarAdapter).balance, 0, - "axelarAdapter should have 0 usdc" + "axelarAdapter should have 0 native" ); - assertEq(user.balance, 0, "user should have 0 usdc"); + assertEq(user.balance, 0, "user should have 0 native"); } function test_RevertWhen_BridgeUnsupportedERC20() public { diff --git a/test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol new file mode 100644 index 0000000..f1fa229 --- /dev/null +++ b/test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.0; + +import {SushiXSwapV2} from "../../src/SushiXSwapV2.sol"; +import {ConnextAdapter} from "../../src/adapters/ConnextAdapter.sol"; +import {ISushiXSwapV2} from "../../src/interfaces/ISushiXSwapV2.sol"; +import {IRouteProcessor} from "../../src/interfaces/IRouteProcessor.sol"; +import {IWETH} from "../../src/interfaces/IWETH.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../../utils/BaseTest.sol"; +import "../../utils/RouteProcessorHelper.sol"; + +contract ConnextAdapterBridgeTest is BaseTest { + using SafeERC20 for IERC20; + + SushiXSwapV2 public sushiXswap; + ConnextAdapter public connextAdapter; + IRouteProcessor public routeProcessor; + RouteProcessorHelper public routeProcessorHelper; + + IWETH public weth; + IERC20 public sushi; + IERC20 public usdc; + IERC20 public usdt; + + uint32 opDestinationDomain = 1869640809; + + address constant NATIVE_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public operator = address(0xbeef); + address public owner = address(0x420); + address public user = address(0x4201); + + function setUp() public override { + forkMainnet(); + super.setUp(); + + weth = IWETH(constants.getAddress("mainnet.weth")); + sushi = IERC20(constants.getAddress("mainnet.sushi")); + usdc = IERC20(constants.getAddress("mainnet.usdc")); + usdt = IERC20(constants.getAddress("mainnet.usdt")); + + routeProcessor = IRouteProcessor( + constants.getAddress("mainnet.routeProcessor") + ); + + routeProcessorHelper = new RouteProcessorHelper( + constants.getAddress("mainnet.v2Factory"), + constants.getAddress("mainnet.v3Factory"), + address(routeProcessor), + address(weth) + ); + + vm.startPrank(owner); + sushiXswap = new SushiXSwapV2(routeProcessor, address(weth)); + + // add operator as privileged + sushiXswap.setPrivileged(operator, true); + + connextAdapter = new ConnextAdapter( + constants.getAddress("mainnet.connext"), + constants.getAddress("mainnet.routeProcessor"), + constants.getAddress("mainnet.weth") + ); + sushiXswap.updateAdapterStatus(address(connextAdapter), true); + + vm.stopPrank(); + } + + function test_RevertWhen_SendingMessage() public { + vm.startPrank(user); + vm.expectRevert(); + sushiXswap.sendMessage(address(connextAdapter), ""); + } + + function testFuzz_BridgeERC20(uint32 amount) public { + vm.assume(amount > 1000000); // > 1 usdc + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(usdc), user, amount); + vm.deal(user, gasNeeded); + + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(usdc), // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic usdc bridge + vm.startPrank(user); + usdc.safeIncreaseAllowance(address(sushiXswap), amount); + + sushiXswap.bridge{value: gasNeeded}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(usdc), + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + "", // swap payload + "" // payload data + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + } + + function test_BridgeUSDT() public { + uint32 amount = 1000000; // 1 usdt + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(usdt), user, amount); + vm.deal(user, gasNeeded); + + // basic usdt bridge + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(usdt), // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic usdc bridge + vm.startPrank(user); + usdt.safeIncreaseAllowance(address(sushiXswap), amount); + + sushiXswap.bridge{value: gasNeeded}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(usdt), + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + "", // swap payload + "" // payload data + ); + + assertEq( + usdt.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdt.balanceOf(user), 0, "user should have 0 usdc"); + } + + function testFuzz_BridgeNative(uint256 amount) public { + vm.assume(amount > 1 ether && amount < 250 ether); // > 1 eth & < 250 eth + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + uint256 valueToSend = amount + gasNeeded; + vm.deal(user, valueToSend); + + // basic usdt bridge + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + NATIVE_ADDRESS, // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic usdc bridge + vm.startPrank(user); + + sushiXswap.bridge{value: valueToSend}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: NATIVE_ADDRESS, + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + "", // swap payload + "" // payload data + ); + + assertEq( + address(connextAdapter).balance, + 0, + "connextAdapter should have 0 native" + ); + assertEq(user.balance, 0, "user should have 0 native"); + } + + function test_RevertWhen_BridgeUnsupportedERC20Connext() public { + uint32 amount = 1000000; // 1 sushi + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(sushi), user, amount); + vm.deal(user, gasNeeded); + + // basic sushi bridge, unsupported token + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(sushi), // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic usdc bridge + vm.startPrank(user); + sushi.safeIncreaseAllowance(address(sushiXswap), amount); + + vm.expectRevert(); + sushiXswap.bridge{value: gasNeeded}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(sushi), + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + "", // swap payload + "" // payload data + ); + } + + function test_BridgeERC20WithSwapData(uint32 amount) public { + uint32 amount = 1000000; // 1 usdc + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(usdc), user, amount); + vm.deal(user, gasNeeded); + + bytes memory computedRoute_dst = routeProcessorHelper.computeRoute( + false, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd_dst = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: 0, // amountIn doesn't matter on dst since we use amount bridged + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute_dst + }); + + bytes memory rpd_encoded_dst = abi.encode(rpd_dst); + + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(usdc), // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic usdc bridge + vm.startPrank(user); + usdc.safeIncreaseAllowance(address(sushiXswap), amount); + + sushiXswap.bridge{value: gasNeeded}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(usdc), + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + rpd_encoded_dst, // swap payload + "" // payload data + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + } + + function test_BridgeNativeWithSwapData() public { + uint64 amount = 1 ether; // 1 eth + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + uint256 valueToSend = amount + gasNeeded; + vm.deal(user, valueToSend); + + bytes memory computedRoute_dst = routeProcessorHelper.computeRoute( + false, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd_dst = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: 0, // amountIn doesn't matter on dst since we use amount bridged + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute_dst + }); + + bytes memory rpd_encoded_dst = abi.encode(rpd_dst); + + // basic usdt bridge + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + NATIVE_ADDRESS, // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic native bridge, get weth on dst + vm.startPrank(user); + + sushiXswap.bridge{value: valueToSend}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: NATIVE_ADDRESS, + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + rpd_encoded_dst, // swap payload + "" // payload data + ); + + assertEq( + address(connextAdapter).balance, + 0, + "connextAdapter should have 0 native" + ); + assertEq(user.balance, 0, "user should have 0 native"); + } + + function test_RevertWhen_BridgeERC20WithNoGasPassed() public { + uint32 amount = 1000000; // 1 usdc + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(usdc), user, amount); + vm.deal(user, gasNeeded); + + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(usdc), // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic usdc bridge + vm.startPrank(user); + usdc.safeIncreaseAllowance(address(sushiXswap), amount); + + vm.expectRevert(bytes4(keccak256("NoGasReceived()"))); + sushiXswap.bridge( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(usdc), + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + "", // swap payload + "" // payload data + ); + } + + function test_RevertWhen_BridgeNativeWithNoGasPassed() public { + uint64 amount = 1 ether; // 1 eth + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + uint256 valueToSend = amount + gasNeeded; + vm.deal(user, valueToSend); + + // basic usdt bridge + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + NATIVE_ADDRESS, // token to bridge + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + // basic native bridge, get weth on dst + vm.startPrank(user); + + vm.expectRevert(bytes4(keccak256("NoGasReceived()"))); + sushiXswap.bridge{value: amount}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: NATIVE_ADDRESS, + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // refundAddress + "", // swap payload + "" // payload data + ); + } +} diff --git a/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol new file mode 100644 index 0000000..04afb62 --- /dev/null +++ b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.0; + +import {SushiXSwapV2} from "../../src/SushiXSwapV2.sol"; +import {ConnextAdapter} from "../../src/adapters/ConnextAdapter.sol"; +import {ISushiXSwapV2} from "../../src/interfaces/ISushiXSwapV2.sol"; +import {IRouteProcessor} from "../../src/interfaces/IRouteProcessor.sol"; +import {IWETH} from "../../src/interfaces/IWETH.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../../utils/BaseTest.sol"; +import "../../utils/RouteProcessorHelper.sol"; + +contract ConnextAdapterSwapAndBridgeTest is BaseTest { + using SafeERC20 for IERC20; + + SushiXSwapV2 public sushiXswap; + ConnextAdapter public connextAdapter; + IRouteProcessor public routeProcessor; + RouteProcessorHelper public routeProcessorHelper; + + IWETH public weth; + IERC20 public sushi; + IERC20 public usdc; + IERC20 public usdt; + + uint32 opDestinationDomain = 1869640809; + + address constant NATIVE_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public operator = address(0xbeef); + address public owner = address(0x420); + address public user = address(0x4201); + + function setUp() public override { + forkMainnet(); + super.setUp(); + + weth = IWETH(constants.getAddress("mainnet.weth")); + sushi = IERC20(constants.getAddress("mainnet.sushi")); + usdc = IERC20(constants.getAddress("mainnet.usdc")); + usdt = IERC20(constants.getAddress("mainnet.usdt")); + + routeProcessor = IRouteProcessor( + constants.getAddress("mainnet.routeProcessor") + ); + + routeProcessorHelper = new RouteProcessorHelper( + constants.getAddress("mainnet.v2Factory"), + constants.getAddress("mainnet.v3Factory"), + address(routeProcessor), + address(weth) + ); + + vm.startPrank(owner); + sushiXswap = new SushiXSwapV2(routeProcessor, address(weth)); + + // add operator as privileged + sushiXswap.setPrivileged(operator, true); + + connextAdapter = new ConnextAdapter( + constants.getAddress("mainnet.connext"), + constants.getAddress("mainnet.routeProcessor"), + constants.getAddress("mainnet.weth") + ); + sushiXswap.updateAdapterStatus(address(connextAdapter), true); + + vm.stopPrank(); + } + + function test_SwapFromERC20ToERC20AndBridgeConnext() public { + // basic swap 1 weth to usdc and bridge + uint64 amount = 1 ether; // 1 weth + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(weth), user, amount); + vm.deal(user, gasNeeded); + + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, // rpHasToken + false, // isV2 + address(weth), // tokenIn + address(usdc), // tokenOut + 500, // fee + address(connextAdapter) // to + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(weth), + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: address(connextAdapter), + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(usdc), // token to bridge + 0, // amount to bridge - 0 since swap first + 300 // slippage tolerance, 3% + ); + + vm.startPrank(user); + IERC20(address(weth)).safeIncreaseAllowance( + address(sushiXswap), + amount + ); + + sushiXswap.swapAndBridge{value: gasNeeded}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(weth), + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // _refundAddress + rpd_encoded, // swap data + "", // swap payload data + "" // payload data + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + } + + function test_SwapFromERC20ToUSDTAndBridge() public { + uint32 amount = 1000000; // 1 usdt + + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(usdt), user, amount); + vm.deal(user, gasNeeded); + + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, // rpHasToken + false, // isV2 + address(usdt), // tokenIn + address(usdc), // tokenOut + 100, // fee + address(connextAdapter) // to + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdt), + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: address(connextAdapter), + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(usdc), // token to bridge + 0, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + vm.startPrank(user); + IERC20(address(usdt)).safeIncreaseAllowance( + address(sushiXswap), + amount + ); + + sushiXswap.swapAndBridge{value: gasNeeded}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(usdt), + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // _refundAddress + rpd_encoded, // swap data + "", // swap payload data + "" // payload data + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + assertEq( + usdt.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdt" + ); + } + + function test_SwapFromNativeToERC20AndBridge() public { + // basic swap 1 eth to usdc and bridge + uint64 amount = 1 ether; // 1 eth + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + uint256 valueToSend = amount + gasNeeded; + vm.deal(user, valueToSend); + + bytes memory computeRoute = routeProcessorHelper.computeRouteNativeIn( + address(weth), // wrapToken + false, // isV2 + address(usdc), // tokenOut + 500, // fee + address(connextAdapter) // to + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: NATIVE_ADDRESS, + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: address(connextAdapter), + route: computeRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + address(usdc), // token to bridge + 0, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + vm.startPrank(user); + sushiXswap.swapAndBridge{value: valueToSend}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: NATIVE_ADDRESS, // doesn't matter what you put for bridge params when swapping first + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // _refundAddress + rpd_encoded, // swap data + "", // swap payload data + "" // payload data + ); + + assertEq( + address(connextAdapter).balance, + 0, + "connextAdapter should have 0 eth" + ); + assertEq(user.balance, 0, "user should have 0 eth"); + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + } + + function test_RevertWhen_SwapFromERC20ToNativeAndBridge() public { + // basic swap 1 usdc to native and bridge + uint32 amount = 1000000; // 1 usdc + uint64 gasNeeded = 0.1 ether; // eth for gas to pass + + deal(address(usdc), user, amount); + vm.deal(user, gasNeeded); + + bytes memory computeRoute = routeProcessorHelper.computeRouteNativeOut( + true, // rpHasToken + false, // isV2 + address(usdc), // tokenIn + address(weth), // tokenOut + 500, // fee + address(connextAdapter) // to + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: NATIVE_ADDRESS, + amountOutMin: 0, + to: address(connextAdapter), + route: computeRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory adapterData = abi.encode( + opDestinationDomain, // dst domain + address(user), // target + address(user), // address for fallback transfers + NATIVE_ADDRESS, // token to bridge + 0, // amouint to bridge + 300 // slippage tolerance, 3% + ); + + vm.startPrank(user); + IERC20(address(usdc)).safeIncreaseAllowance( + address(sushiXswap), + amount + ); + + vm.expectRevert(bytes4(keccak256("RpSentNativeIn()"))); + sushiXswap.swapAndBridge{value: gasNeeded}( + ISushiXSwapV2.BridgeParams({ + refId: 0x0000, + adapter: address(connextAdapter), + tokenIn: address(weth), // doesn't matter what you put for bridge params when swapping first + amountIn: amount, + to: user, + adapterData: adapterData + }), + user, // _refundAddress + rpd_encoded, // swap data + "", // swap payload data + "" // payload data + ); + } +} diff --git a/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol new file mode 100644 index 0000000..69429e8 --- /dev/null +++ b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol @@ -0,0 +1,1155 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.0; + +import {SushiXSwapV2} from "../../src/SushiXSwapV2.sol"; +import {ConnextAdapter} from "../../src/adapters/ConnextAdapter.sol"; +import {AirdropPayloadExecutor} from "../../src/payload-executors/AirdropPayloadExecutor.sol"; +import {ISushiXSwapV2} from "../../src/interfaces/ISushiXSwapV2.sol"; +import {ISushiXSwapV2Adapter} from "../../src/interfaces/ISushiXSwapV2Adapter.sol"; +import {IRouteProcessor} from "../../src/interfaces/IRouteProcessor.sol"; +import {IWETH} from "../../src/interfaces/IWETH.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../../utils/BaseTest.sol"; +import "../../utils/RouteProcessorHelper.sol"; + +contract ConnextAdapterXReceiveTest is BaseTest { + using SafeERC20 for IERC20; + + SushiXSwapV2 public sushiXswap; + ConnextAdapter public connextAdapter; + AirdropPayloadExecutor public airdropExecutor; + IRouteProcessor public routeProcessor; + RouteProcessorHelper public routeProcessorHelper; + + address public connext; + + IWETH public weth; + IERC20 public sushi; + IERC20 public usdc; + IERC20 public usdt; + + uint32 opDestinationDomain = 1869640809; + + address constant NATIVE_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public operator = address(0xbeef); + address public owner = address(0x420); + address public user = address(0x4201); + + function setUp() public override { + forkMainnet(); + super.setUp(); + + weth = IWETH(constants.getAddress("mainnet.weth")); + sushi = IERC20(constants.getAddress("mainnet.sushi")); + usdc = IERC20(constants.getAddress("mainnet.usdc")); + usdt = IERC20(constants.getAddress("mainnet.usdt")); + + connext = constants.getAddress("mainnet.connext"); + + routeProcessor = IRouteProcessor( + constants.getAddress("mainnet.routeProcessor") + ); + + routeProcessorHelper = new RouteProcessorHelper( + constants.getAddress("mainnet.v2Factory"), + constants.getAddress("mainnet.v3Factory"), + address(routeProcessor), + address(weth) + ); + + vm.startPrank(owner); + sushiXswap = new SushiXSwapV2(routeProcessor, address(weth)); + + // add operator as privileged + sushiXswap.setPrivileged(operator, true); + + connextAdapter = new ConnextAdapter( + constants.getAddress("mainnet.connext"), + constants.getAddress("mainnet.routeProcessor"), + constants.getAddress("mainnet.weth") + ); + sushiXswap.updateAdapterStatus(address(connextAdapter), true); + + // deploy payload executors + airdropExecutor = new AirdropPayloadExecutor(); + + vm.stopPrank(); + } + + function test_RevertWhen_ReceivedCallFromNonStargateComposer() public { + vm.prank(owner); + vm.expectRevert(); + connextAdapter.xReceive( + bytes32(""), + 0, + address(0), + address(0), + uint32(0), + bytes("") + ); + } + + function testFuzz_ReceiveERC20SwapToERC20(uint32 amount) public { + vm.assume(amount > 1000000); // > 1 usdc + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + // auto sends enough gas, so no need to calculate gasNeeded & send here + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertGt(weth.balanceOf(user), 0, "user should have > 0 weth"); + } + + function test_ReceiveWethUnwrapIntoNativeWithRP() public {} + + function test_ReceiveExtraERC20SwapToERC20UserReceivesExtra() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount + 1); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + // auto sends enough gas, so no need to calculate gasNeeded & send here + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 1, "user should have extra usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertGt(weth.balanceOf(user), 0, "user should have > 0 weth"); + } + + function test_ReceiveUSDTSwapToERC20() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdt), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdt), + address(usdc), + 100, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdt), + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + // auto sends enough gas, so no need to calculate gasNeeded & send here + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdt), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdt.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdt" + ); + assertEq(usdt.balanceOf(user), 0, "user should have 0 usdt"); + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertGt(usdc.balanceOf(user), 0, "user should have > 0 usdc"); + } + + function test_ReceiveERC20AndNativeSwapToERC20ReturnDust() public { + uint32 amount = 1000000; // 1 USDC + uint64 nativeAmount = 0.001 ether; + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + vm.deal(address(connextAdapter), nativeAmount); + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + // auto sends enough gas, so no need to calculate gasNeeded & send here + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertGt(weth.balanceOf(user), 0, "user should have > 0 weth"); + assertEq( + address(connextAdapter).balance, + 0, + "adapter should have 0 eth" + ); + assertEq(user.balance, nativeAmount, "user should have all dust eth"); + } + + function test_ReceiveERC20SwapToNative() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRouteNativeOut( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: NATIVE_ADDRESS, + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + // auto sends enough gas, so no need to calculate gasNeeded & send here + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + assertEq( + address(connextAdapter).balance, + 0, + "connextAdapter should have 0 eth" + ); + assertGt(user.balance, 0, "user should have > 0 eth"); + } + + function test_ReceiveERC20NotEnoughGasForSwap() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 90000}( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + } + + function test_ReceiveUSDTNotEnoughGasForSwap() public { + uint32 amount = 1000000; // 1 USDT + + deal(address(usdt), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdt and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdt), + address(usdc), + 100, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdt), + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 90000}( + bytes32("000303"), + amount, + address(usdt), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdt.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdt" + ); + assertEq(usdt.balanceOf(user), amount, "user should have all usdt"); + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + } + + function test_ReceiveERC20AndNativeNotEnoughGasForSwapConnext() public { + uint32 amount = 1000000; // 1 USDC + uint64 nativeAmount = 0.001 ether; // + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + vm.deal(address(connextAdapter), nativeAmount); + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 90000}( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + assertEq( + address(connextAdapter).balance, + 0, + "adapter should have 0 eth" + ); + assertEq(user.balance, nativeAmount, "user should have all dust eth"); + } + + function test_ReceiveERC20EnoughForGasNoSwapOrPayloadData() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + bytes memory payload = abi.encode( + user, // to + "", // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + // auto sends enough gas, so no need to calculate gasNeeded & send here + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + } + + function test_ReceiveERC20FailedSwap() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // switched tokenIn to weth, and tokenOut to usdc - should fail on swap + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(weth), + address(usdc), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(weth), + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + // auto sends enough gas, so no need to calculate gasNeeded & send here + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + } + + function test_ReceiveUSDCAndNativeFailedSwapMinimumGasSent() public { + uint32 amount = 1000000; // 1 USDC + uint64 dustAmount = 0.2 ether; + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + vm.deal(address(connextAdapter), dustAmount); + + // switched tokenIn to weth, and tokenOut to usdc - should fail now on swap + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(weth), + address(usdc), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(weth), + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 103384}( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq( + address(connextAdapter).balance, + 0, + "adapter should have 0 native" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq(user.balance, dustAmount, "user should have all the dust"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + } + + function test_ReceiveERC20FailedSwapFromOutOfGas() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 120000}( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + } + + function test_ReceiveERC20FailedSwapSlippageCheck() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + user + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: type(uint256).max, + to: user, + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + } + + function test_ReceiveERC20SwapToERC20AirdropERC20FromPayload() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + address(airdropExecutor) + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: address(airdropExecutor), + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + // airdrop payload data + address user1 = address(0x4203); + address user2 = address(0x4204); + address[] memory recipients = new address[](2); + recipients[0] = user1; + recipients[1] = user2; + + bytes memory payloadData = abi.encode( + ISushiXSwapV2Adapter.PayloadData({ + target: address(airdropExecutor), + gasLimit: 200000, + targetData: abi.encode( + AirdropPayloadExecutor.AirdropPayloadParams({ + token: address(weth), + recipients: recipients + }) + ) + }) + ); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + payloadData // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + assertGt(weth.balanceOf(user1), 0, "user1 should have > 0 weth"); + assertGt(weth.balanceOf(user2), 0, "user2 should have > 0 weth"); + } + + function test_ReceiveERC20SwapToERC20FailedAirdropFromPayload() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // receive 1 usdc and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdc), + address(weth), + 500, + address(airdropExecutor) + ); + + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: address(airdropExecutor), + route: computedRoute + }); + + bytes memory rpd_encoded = abi.encode(rpd); + + // airdrop payload data + address user1 = address(0x4203); + address user2 = address(0x4204); + address[] memory recipients = new address[](2); + recipients[0] = user1; + recipients[1] = user2; + + bytes memory payloadData = abi.encode( + ISushiXSwapV2Adapter.PayloadData({ + target: address(airdropExecutor), + gasLimit: 200000, + targetData: abi.encode( + AirdropPayloadExecutor.AirdropPayloadParams({ + token: address(user), // using user for token so it fails + recipients: recipients + }) + ) + }) + ); + + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + payloadData // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq( + weth.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 weth" + ); + assertEq(weth.balanceOf(user), 0, "user should have 0 weth"); + assertEq(weth.balanceOf(user1), 0, "user1 should have 0 weth"); + assertEq(weth.balanceOf(user2), 0, "user2 should have 0 weth"); + } + + function test_ReceiveERC20AirdropFromPayload() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // airdrop payload data + address user1 = address(0x4203); + address user2 = address(0x4204); + address[] memory recipients = new address[](2); + recipients[0] = user1; + recipients[1] = user2; + + bytes memory payloadData = abi.encode( + ISushiXSwapV2Adapter.PayloadData({ + target: address(airdropExecutor), + gasLimit: 200000, + targetData: abi.encode( + AirdropPayloadExecutor.AirdropPayloadParams({ + token: address(usdc), + recipients: recipients + }) + ) + }) + ); + + bytes memory payload = abi.encode( + user, // to + "", // _swapData + payloadData // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), 0, "user should have 0 usdc"); + assertGt(usdc.balanceOf(user1), 0, "user1 should have > 0 usdc"); + assertGt(usdc.balanceOf(user2), 0, "user2 should have > 0 usdc"); + } + + function test_ReceiveERC20FailedAirdropFromPayload() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // airdrop payload data + address user1 = address(0x4203); + address user2 = address(0x4204); + address[] memory recipients = new address[](2); + recipients[0] = user1; + recipients[1] = user2; + + bytes memory payloadData = abi.encode( + ISushiXSwapV2Adapter.PayloadData({ + target: address(airdropExecutor), + gasLimit: 200000, + targetData: abi.encode( + AirdropPayloadExecutor.AirdropPayloadParams({ + token: address(weth), // using weth for token to airdrop so it fail + recipients: recipients + }) + ) + }) + ); + + bytes memory payload = abi.encode( + user, // to + "", // _swapData + payloadData // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq(usdc.balanceOf(user1), 0, "user1 should have 0 usdc"); + assertEq(usdc.balanceOf(user2), 0, "user2 should have 0 usdc"); + } + + function test_ReceiveERC20FailedAirdropPayloadFromOutOfGas() public { + uint32 amount = 1000000; // 1 USDC + + deal(address(usdc), address(connextAdapter), amount); // amount adapter receives + + // airdrop payload data + address user1 = address(0x4203); + address user2 = address(0x4204); + address[] memory recipients = new address[](2); + recipients[0] = user1; + recipients[1] = user2; + + bytes memory payloadData = abi.encode( + ISushiXSwapV2Adapter.PayloadData({ + target: address(airdropExecutor), + gasLimit: 200000, + targetData: abi.encode( + AirdropPayloadExecutor.AirdropPayloadParams({ + token: address(usdc), + recipients: recipients + }) + ) + }) + ); + + bytes memory payload = abi.encode( + user, // to + "", // _swapData + payloadData // _payloadData + ); + + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 120000}( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); + + assertEq( + usdc.balanceOf(address(connextAdapter)), + 0, + "connextAdapter should have 0 usdc" + ); + assertEq(usdc.balanceOf(user), amount, "user should have all usdc"); + assertEq(usdc.balanceOf(user1), 0, "user1 should have 0 usdc"); + assertEq(usdc.balanceOf(user2), 0, "user2 should have 0 usdc"); + } +} diff --git a/utils/Constants.sol b/utils/Constants.sol index 2e334c1..8bb9eab 100644 --- a/utils/Constants.sol +++ b/utils/Constants.sol @@ -35,6 +35,8 @@ contract Constants { setAddress("mainnet.cctpTokenMessenger", 0xBd3fa81B58Ba92a82136038B25aDec7066af3155); setAddress("mainnet.squidRouter", 0xce16F69375520ab01377ce7B88f5BA8C48F8D666); + + setAddress("mainnet.connext", 0x8898B472C54c31894e3B9bb83cEA802a5d0e63C6); } function initAddressLabels(Vm vm) public {