From e9ee45cab59f22404fee17d9ce99a3db8b86a98a Mon Sep 17 00:00:00 2001 From: 0xJiro Date: Fri, 3 Nov 2023 23:27:04 -0400 Subject: [PATCH 1/7] added connext adapter contract --- src/adapters/ConnextAdapter.sol | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/adapters/ConnextAdapter.sol diff --git a/src/adapters/ConnextAdapter.sol b/src/adapters/ConnextAdapter.sol new file mode 100644 index 0000000..f4f1197 --- /dev/null +++ b/src/adapters/ConnextAdapter.sol @@ -0,0 +1,18 @@ +// 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 "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + + + +contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { + using SafeERC20 for IERC20; + + +} \ No newline at end of file From b8564e5a65304e242b867ae516ebe1e24799ff49 Mon Sep 17 00:00:00 2001 From: 0xJiro Date: Fri, 3 Nov 2023 23:28:03 -0400 Subject: [PATCH 2/7] forge install: interfaces v2.0.5 --- .gitmodules | 3 +++ lib/interfaces | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/interfaces 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 From fa99cf9cd382f98b03e5078762978a52080fb134 Mon Sep 17 00:00:00 2001 From: 0xJiro Date: Sat, 4 Nov 2023 21:42:59 -0400 Subject: [PATCH 3/7] added connext adapter logic --- remappings.txt | 3 +- src/adapters/ConnextAdapter.sol | 155 +++++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 3 deletions(-) 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 index f4f1197..e058cdf 100644 --- a/src/adapters/ConnextAdapter.sol +++ b/src/adapters/ConnextAdapter.sol @@ -5,8 +5,8 @@ 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/ICONNEXT.sol"; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -14,5 +14,156 @@ import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { using SafeERC20 for IERC20; + IConnext public immutable; + 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(); + + 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 + ); + + bytes memory payload = bytes(""); + if (_swapData.length > 0 || _payloadData.length > 0) { + payload = abi.encode(params.to, _swapData, _payloadData); + } + + 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 returns (bytes memory) { + uint256 gasLeft = gasLeft(); + + // todo: check that msg sender does come from connext contract? + + (address to, bytes memory _swapData, bytes memory _payloadData) = abi + .decode(payload, (address, bytes, bytes)); + + uint256 reserveGas = 100000; + } + + } \ No newline at end of file From 1165d7d127826f2dcbb81fd32ac8a2529295aaf7 Mon Sep 17 00:00:00 2001 From: 0xJiro Date: Sat, 4 Nov 2023 22:52:38 -0400 Subject: [PATCH 4/7] added skeleton for connext tests --- src/adapters/ConnextAdapter.sol | 67 +++++++++-- .../ConnextAdapterBridgeTest.t.sol | 85 +++++++++++++ .../ConnextAdapterSwapAndBridgeTest.t.sol | 75 ++++++++++++ .../ConnextAdapterXReceiveTest.t.sol | 112 ++++++++++++++++++ utils/Constants.sol | 2 + 5 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol create mode 100644 test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol create mode 100644 test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol diff --git a/src/adapters/ConnextAdapter.sol b/src/adapters/ConnextAdapter.sol index e058cdf..112c615 100644 --- a/src/adapters/ConnextAdapter.sol +++ b/src/adapters/ConnextAdapter.sol @@ -6,7 +6,7 @@ import "../interfaces/IRouteProcessor.sol"; import "../interfaces/IWETH.sol"; import {IXReceiver} from "connext-interfaces/core/IXReceiver.sol"; -import {IConnext} from "connext-interfaces/ICONNEXT.sol"; +import {IConnext} from "connext-interfaces/core/IConnext.sol"; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -14,7 +14,7 @@ import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { using SafeERC20 for IERC20; - IConnext public immutable; + IConnext public immutable connext; IRouteProcessor public immutable rp; IWETH public immutable weth; @@ -31,6 +31,7 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { } error RpSentNativeIn(); + error NotConnext(); constructor( address _connext, @@ -123,10 +124,8 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { params.amount ); - bytes memory payload = bytes(""); - if (_swapData.length > 0 || _payloadData.length > 0) { - payload = abi.encode(params.to, _swapData, _payloadData); - } + // build payload from params.to, _swapData, and _payloadData + bytes memory payload = abi.encode(params.to, _swapData, _payloadData); connext.xcall{value: address(this).balance} ( params.destinationDomain, @@ -145,7 +144,7 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { /// @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 + /// @param _callData data received from source chain function xReceive( bytes32 _transferId, uint256 _amount, @@ -153,17 +152,61 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { address _originSender, uint32 _origin, bytes memory _callData - ) external returns (bytes memory) { - uint256 gasLeft = gasLeft(); + ) external override returns (bytes memory) { + uint256 gasLeft = gasleft(); + if (msg.sender != address(connext)) + revert NotConnext(); // todo: check that msg sender does come from connext contract? (address to, bytes memory _swapData, bytes memory _payloadData) = abi - .decode(payload, (address, bytes, bytes)); + .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)}(""); + } + + // 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) {} + } - uint256 reserveGas = 100000; + 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/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol new file mode 100644 index 0000000..732176d --- /dev/null +++ b/test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol @@ -0,0 +1,85 @@ +// 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; + + 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 {} + + function test_BridgeERC20() public {} + + function test_BridgeUSDT() public {} + + function test_BridgeNative() public {} + + function test_RevertWhen_BridgeUnsupportedERC20() public {} + + function test_BridgeERC20WithSwapData() public {} + + function test_BridgeNativeWithSwapData() public {} + + function test_RevertWhen_BridgeERC20WithNoGasPassed() public {} + + function test_RevertWhen_BridgeNativeWithNoGasPassed() public {} +} diff --git a/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol new file mode 100644 index 0000000..6ff0cf5 --- /dev/null +++ b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol @@ -0,0 +1,75 @@ +// 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; + + 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_SwapFromERC20ToERC20AndBridge() public {} + + function test_SwapFromERC20ToUSDTAndBridge() public {} + + function test_SwapFromNativeToERC20AndBridge() public {} + + function test_RevertWhen_SwapFromERC20ToNativeAndBridge() public {} +} diff --git a/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol new file mode 100644 index 0000000..6d422f0 --- /dev/null +++ b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol @@ -0,0 +1,112 @@ +// 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 {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; + + 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_ReceiveERC20SwapToERC20() public {} + + function test_ReceiveExtraERC20SwapToERC20UserReceivesExtra() public {} + + function test_ReceiveUSDTSwapToERC20() public {} + + function test_ReceiveERC20AndNativeSwapToERC20ReturnDust() public {} + + function test_ReceiveERC20SwapToNative() public {} + + function test_ReceiveERC20NotEnoughGasForSwap() public {} + + function test_ReceiveUSDTNotEnoughGasForSwap() public {} + + function test_ReceiveERC20AndNativeNotEnoughGasForSwap() public {} + + function test_ReceiveERC20EnoughForGasNoSwapOrPayloadData() public {} + + function test_ReceiveERC20FailedSwap() public {} + + function test_ReceiveUSDCAndNativeFailedSwapMinimumGasSent() public {} + + function test_ReceiveERC20FailedSwapFromOutOfGas() public {} + + function test_ReceiveERC20FailedSwapSlippageCheck() public {} + + function test_ReceiveERC20SwapToERC20AirdropERC20FromPayload() public {} + + function test_ReceiveERC20SwapToERC20FailedAirdropFromPayload() public {} + + function test_ReceiveERC20AirdropFromPayload() public {} + + function test_ReceiveERC20FailedAirdropFromPayload() public {} + + function test_ReceiveERC20FailedAirdropPayloadFromOutOfGas() public {} +} 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 { From 5e0d1fba9f04bc8a6b4602d22f01028ae455e0a2 Mon Sep 17 00:00:00 2001 From: 0xJiro Date: Sun, 5 Nov 2023 00:37:48 -0400 Subject: [PATCH 5/7] added connext bridge tests --- Makefile | 2 +- src/adapters/ConnextAdapter.sol | 5 + .../AxelarAdapterBridgeTest.t.sol | 4 +- .../ConnextAdapterBridgeTest.t.sol | 370 +++++++++++++++++- .../ConnextAdapterXReceiveTest.t.sol | 3 + 5 files changed, 372 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 9bd51c8..c057d87 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ init: git submodule update --init --recursive forge install test: - forge test -vv + forge test -vv --match-contract ConnextAdapterBridgeTest test-gas-report: forge test -vv --gas-report trace: diff --git a/src/adapters/ConnextAdapter.sol b/src/adapters/ConnextAdapter.sol index 112c615..0f3e6bb 100644 --- a/src/adapters/ConnextAdapter.sol +++ b/src/adapters/ConnextAdapter.sol @@ -32,6 +32,7 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { error RpSentNativeIn(); error NotConnext(); + error NoGasReceived(); constructor( address _connext, @@ -127,6 +128,10 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { // 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, 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 index 732176d..f1fa229 100644 --- a/test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol +++ b/test/ConnextAdapterTests/ConnextAdapterBridgeTest.t.sol @@ -23,6 +23,8 @@ contract ConnextAdapterBridgeTest is BaseTest { IERC20 public usdc; IERC20 public usdt; + uint32 opDestinationDomain = 1869640809; + address constant NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address public operator = address(0xbeef); @@ -65,21 +67,371 @@ contract ConnextAdapterBridgeTest is BaseTest { vm.stopPrank(); } - function test_RevertWhen_SendingMessage() public {} + 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 - function test_BridgeERC20() public {} + deal(address(usdc), user, amount); + vm.deal(user, gasNeeded); - function test_BridgeUSDT() public {} + 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% + ); - function test_BridgeNative() public {} + // basic usdc bridge + vm.startPrank(user); + usdc.safeIncreaseAllowance(address(sushiXswap), amount); - function test_RevertWhen_BridgeUnsupportedERC20() public {} + 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_BridgeERC20WithSwapData() public {} + function test_RevertWhen_BridgeNativeWithNoGasPassed() public { + uint64 amount = 1 ether; // 1 eth + uint64 gasNeeded = 0.1 ether; // eth for gas to pass - function test_BridgeNativeWithSwapData() public {} + uint256 valueToSend = amount + gasNeeded; + vm.deal(user, valueToSend); - function test_RevertWhen_BridgeERC20WithNoGasPassed() public {} + // 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); - function test_RevertWhen_BridgeNativeWithNoGasPassed() public {} + 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/ConnextAdapterXReceiveTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol index 6d422f0..24d5775 100644 --- a/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol +++ b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol @@ -76,6 +76,9 @@ contract ConnextAdapterXReceiveTest is BaseTest { function test_ReceiveERC20SwapToERC20() public {} + function test_ReceiveWethUnwrapIntoNativeWithRP() public { + } + function test_ReceiveExtraERC20SwapToERC20UserReceivesExtra() public {} function test_ReceiveUSDTSwapToERC20() public {} From 85c090456060e4b894d79246b4fc983cbc5b65b3 Mon Sep 17 00:00:00 2001 From: 0xJiro Date: Sun, 5 Nov 2023 01:39:33 -0400 Subject: [PATCH 6/7] added connext swap and bridge tests --- .../ConnextAdapterSwapAndBridgeTest.t.sol | 275 +++++++++++++++++- .../ConnextAdapterXReceiveTest.t.sol | 2 + 2 files changed, 273 insertions(+), 4 deletions(-) diff --git a/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol index 6ff0cf5..5b8bd14 100644 --- a/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol +++ b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol @@ -23,6 +23,8 @@ contract ConnextAdapterSwapAndBridgeTest is BaseTest { IERC20 public usdc; IERC20 public usdt; + uint32 opDestinationDomain = 1869640809; + address constant NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address public operator = address(0xbeef); @@ -65,11 +67,276 @@ contract ConnextAdapterSwapAndBridgeTest is BaseTest { vm.stopPrank(); } - function test_SwapFromERC20ToERC20AndBridge() public {} + function test_SwapFromERC20ToERC20AndBridge() 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 + amount, // amouint to bridge + 300 // slippage tolerance, 3% + ); - function test_SwapFromERC20ToUSDTAndBridge() public {} + 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_SwapFromNativeToERC20AndBridge() public {} + function test_SwapFromERC20ToUSDTAndBridge() public { + uint32 amount = 1000000; // 1 usdt - function test_RevertWhen_SwapFromERC20ToNativeAndBridge() public {} + 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 + amount, // 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 + amount, // 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 + amount, // 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 index 24d5775..661b86a 100644 --- a/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol +++ b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol @@ -27,6 +27,8 @@ contract ConnextAdapterXReceiveTest is BaseTest { IERC20 public usdc; IERC20 public usdt; + uint32 opDestinationDomain = 1869640809; + address constant NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address public operator = address(0xbeef); From 06fc9f13e16892f01886cfaf7911d6166f5e6705 Mon Sep 17 00:00:00 2001 From: 0xJiro Date: Sun, 5 Nov 2023 19:17:58 -0500 Subject: [PATCH 7/7] added swapAndBridge & receives tests for connext adapter --- Makefile | 2 +- src/adapters/ConnextAdapter.sol | 4 +- .../ConnextAdapterSwapAndBridgeTest.t.sol | 10 +- .../ConnextAdapterXReceiveTest.t.sol | 1076 ++++++++++++++++- 4 files changed, 1065 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index c057d87..9bd51c8 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ init: git submodule update --init --recursive forge install test: - forge test -vv --match-contract ConnextAdapterBridgeTest + forge test -vv test-gas-report: forge test -vv --gas-report trace: diff --git a/src/adapters/ConnextAdapter.sol b/src/adapters/ConnextAdapter.sol index 0f3e6bb..866db23 100644 --- a/src/adapters/ConnextAdapter.sol +++ b/src/adapters/ConnextAdapter.sol @@ -161,8 +161,6 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { uint256 gasLeft = gasleft(); if (msg.sender != address(connext)) revert NotConnext(); - - // todo: check that msg sender does come from connext contract? (address to, bytes memory _swapData, bytes memory _payloadData) = abi .decode(_callData, (address, bytes, bytes)); @@ -175,6 +173,8 @@ contract ConnextAdapter is ISushiXSwapV2Adapter, IXReceiver { /// @dev transfer any native token if (address(this).balance > 0) to.call{value: (address(this).balance)}(""); + + return bytes(""); } // 100000 -> exit gas diff --git a/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol index 5b8bd14..04afb62 100644 --- a/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol +++ b/test/ConnextAdapterTests/ConnextAdapterSwapAndBridgeTest.t.sol @@ -67,7 +67,7 @@ contract ConnextAdapterSwapAndBridgeTest is BaseTest { vm.stopPrank(); } - function test_SwapFromERC20ToERC20AndBridge() public { + 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 @@ -101,7 +101,7 @@ contract ConnextAdapterSwapAndBridgeTest is BaseTest { address(user), // target address(user), // address for fallback transfers address(usdc), // token to bridge - amount, // amouint to bridge + 0, // amount to bridge - 0 since swap first 300 // slippage tolerance, 3% ); @@ -174,7 +174,7 @@ contract ConnextAdapterSwapAndBridgeTest is BaseTest { address(user), // target address(user), // address for fallback transfers address(usdc), // token to bridge - amount, // amouint to bridge + 0, // amouint to bridge 300 // slippage tolerance, 3% ); @@ -245,7 +245,7 @@ contract ConnextAdapterSwapAndBridgeTest is BaseTest { address(user), // target address(user), // address for fallback transfers address(usdc), // token to bridge - amount, // amouint to bridge + 0, // amouint to bridge 300 // slippage tolerance, 3% ); @@ -313,7 +313,7 @@ contract ConnextAdapterSwapAndBridgeTest is BaseTest { address(user), // target address(user), // address for fallback transfers NATIVE_ADDRESS, // token to bridge - amount, // amouint to bridge + 0, // amouint to bridge 300 // slippage tolerance, 3% ); diff --git a/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol index 661b86a..69429e8 100644 --- a/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol +++ b/test/ConnextAdapterTests/ConnextAdapterXReceiveTest.t.sol @@ -5,6 +5,7 @@ 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"; @@ -76,42 +77,1079 @@ contract ConnextAdapterXReceiveTest is BaseTest { vm.stopPrank(); } - function test_ReceiveERC20SwapToERC20() public {} + 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 + }); - function test_ReceiveWethUnwrapIntoNativeWithRP() public { + 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_ReceiveExtraERC20SwapToERC20UserReceivesExtra() public {} + 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 + ); - function test_ReceiveUSDTSwapToERC20() public {} + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); - function test_ReceiveERC20AndNativeSwapToERC20ReturnDust() public {} + bytes memory rpd_encoded = abi.encode(rpd); - function test_ReceiveERC20SwapToNative() public {} + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); - function test_ReceiveERC20NotEnoughGasForSwap() public {} + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 90000}( + bytes32("000303"), + amount, + address(usdc), + address(connextAdapter), + opDestinationDomain, + payload + ); - function test_ReceiveUSDTNotEnoughGasForSwap() public {} + 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_ReceiveERC20AndNativeNotEnoughGasForSwap() public {} + function test_ReceiveUSDTNotEnoughGasForSwap() public { + uint32 amount = 1000000; // 1 USDT - function test_ReceiveERC20EnoughForGasNoSwapOrPayloadData() public {} + deal(address(usdt), address(connextAdapter), amount); // amount adapter receives - function test_ReceiveERC20FailedSwap() public {} + // receive 1 usdt and swap to weth + bytes memory computedRoute = routeProcessorHelper.computeRoute( + true, + false, + address(usdt), + address(usdc), + 100, + user + ); - function test_ReceiveUSDCAndNativeFailedSwapMinimumGasSent() public {} + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdt), + amountIn: amount, + tokenOut: address(usdc), + amountOutMin: 0, + to: user, + route: computedRoute + }); - function test_ReceiveERC20FailedSwapFromOutOfGas() public {} + bytes memory rpd_encoded = abi.encode(rpd); - function test_ReceiveERC20FailedSwapSlippageCheck() public {} + bytes memory payload = abi.encode( + user, // to + rpd_encoded, // _swapData + "" // _payloadData + ); - function test_ReceiveERC20SwapToERC20AirdropERC20FromPayload() public {} + vm.prank(constants.getAddress("mainnet.connext")); + connextAdapter.xReceive{gas: 90000}( + bytes32("000303"), + amount, + address(usdt), + address(connextAdapter), + opDestinationDomain, + payload + ); - function test_ReceiveERC20SwapToERC20FailedAirdropFromPayload() public {} + 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_ReceiveERC20AirdropFromPayload() public {} + 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 + ); - function test_ReceiveERC20FailedAirdropFromPayload() public {} + IRouteProcessor.RouteProcessorData memory rpd = IRouteProcessor + .RouteProcessorData({ + tokenIn: address(usdc), + amountIn: amount, + tokenOut: address(weth), + amountOutMin: 0, + to: user, + route: computedRoute + }); - function test_ReceiveERC20FailedAirdropPayloadFromOutOfGas() public {} + 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"); + } }