From 5eaccd10534e948065a3143067961bc602464b30 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:05:47 +0000 Subject: [PATCH 01/12] feat: scaffold finalToken, forwardTo in ZapDataV1 --- .../contracts/libs/ZapDataV1.sol | 20 +++++++ .../router/SynapseIntentPreviewer.sol | 15 +++++ .../contracts/zaps/TokenZapV1.sol | 3 +- .../test/harnesses/ZapDataV1Harness.sol | 12 +++- .../contracts-rfq/test/libs/ZapDataV1.t.sol | 55 +++++++++++++++---- .../test/router/SynapseIntentPreviewer.t.sol | 15 +++++ .../contracts-rfq/test/zaps/TokenZapV1.t.sol | 2 +- 7 files changed, 107 insertions(+), 15 deletions(-) diff --git a/packages/contracts-rfq/contracts/libs/ZapDataV1.sol b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol index 0b7c13a9d1..ceadb37890 100644 --- a/packages/contracts-rfq/contracts/libs/ZapDataV1.sol +++ b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol @@ -44,6 +44,14 @@ library ZapDataV1 { /// This will usually be `4 + 32 * n`, where `n` is the position of the token amount in /// the list of parameters of the target function (starting from 0). /// Or `AMOUNT_NOT_PRESENT` if the token amount is not encoded within `payload_`. + /// @param finalToken_ The token produced as a result of the Zap action (ERC20 or native gas token). + /// A zero address value signals that the Zap action doesn't result in any asset per se, + /// like bridging or depositing into a vault without an LP token. + /// Note: this parameter must be set to a non-zero value if the `forwardTo_` parameter is + /// set to a non-zero value. + /// @param forwardTo_ The address to which `finalToken` should be forwarded. This parameter is required only + /// if the Zap action does not automatically transfer the token to the intended recipient. + /// Otherwise, it must be set to address(0). /// @param target_ Address of the target contract. /// @param payload_ ABI-encoded calldata to be used for the `target_` contract call. /// If the target function has the token amount as an argument, any placeholder amount value @@ -51,6 +59,8 @@ library ZapDataV1 { /// be replaced with the actual amount, when the Zap Data is decoded. function encodeV1( uint16 amountPosition_, + address finalToken_, + address forwardTo_, address target_, bytes memory payload_ ) @@ -74,6 +84,16 @@ library ZapDataV1 { } } + /// @notice Extracts the finalToken address from the encoded Zap Data. + function finalToken(bytes calldata encodedZapData) internal pure returns (address finalToken_) { + // TODO + } + + /// @notice Extracts the forwardTo address from the encoded Zap Data. + function forwardTo(bytes calldata encodedZapData) internal pure returns (address forwardTo_) { + // TODO + } + /// @notice Extracts the target address from the encoded Zap Data. function target(bytes calldata encodedZapData) internal pure returns (address target_) { // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol index caec160b36..0d0a3e5a69 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol @@ -152,6 +152,9 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: pool, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // addLiquidity(amounts, minToMint, deadline) payload_: abi.encodeCall(IDefaultExtendedPool.addLiquidity, (amounts, 0, type(uint256).max)), // amountIn is encoded within `amounts` at `TOKEN_IN_INDEX`, `amounts` is encoded after @@ -185,6 +188,9 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: pool, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) payload_: abi.encodeCall( IDefaultExtendedPool.removeLiquidityOneToken, (0, params.tokenIndexTo, 0, type(uint256).max) @@ -236,6 +242,9 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: params.pool, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) payload_: abi.encodeCall( IDefaultPool.swap, (params.tokenIndexFrom, params.tokenIndexTo, 0, 0, type(uint256).max) @@ -261,6 +270,9 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: amountIn, zapData: ZapDataV1.encodeV1({ target_: wrappedNative, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // deposit() payload_: abi.encodeCall(IWETH9.deposit, ()), // amountIn is not encoded @@ -281,6 +293,9 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: wrappedNative, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // withdraw(amount) payload_: abi.encodeCall(IWETH9.withdraw, (0)), // amountIn encoded as the first parameter diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol index bd58d8f391..697dd4e327 100644 --- a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -119,7 +119,8 @@ contract TokenZapV1 is IZapRecipient { amountPosition = ZapDataV1.AMOUNT_NOT_PRESENT; } // At this point, we have checked that both `amountPosition` and `payload.length` fit in uint16. - return ZapDataV1.encodeV1(uint16(amountPosition), target, payload); + // TODO + return ZapDataV1.encodeV1(uint16(amountPosition), address(0), address(0), target, payload); } /// @notice Decodes the ZapData for a Zap action. Replaces the placeholder amount with the actual amount, diff --git a/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol index 727e70fb22..8414fb454b 100644 --- a/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol +++ b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol @@ -13,6 +13,8 @@ contract ZapDataV1Harness { function encodeV1( uint16 amountPosition_, + address finalToken_, + address forwardTo_, address target_, bytes memory payload_ ) @@ -20,13 +22,21 @@ contract ZapDataV1Harness { pure returns (bytes memory encodedZapData) { - return ZapDataV1.encodeV1(amountPosition_, target_, payload_); + return ZapDataV1.encodeV1(amountPosition_, finalToken_, forwardTo_, target_, payload_); } function version(bytes calldata encodedZapData) public pure returns (uint16) { return ZapDataV1.version(encodedZapData); } + function finalToken(bytes calldata encodedZapData) public pure returns (address) { + return ZapDataV1.finalToken(encodedZapData); + } + + function forwardTo(bytes calldata encodedZapData) public pure returns (address) { + return ZapDataV1.forwardTo(encodedZapData); + } + function target(bytes calldata encodedZapData) public pure returns (address) { return ZapDataV1.target(encodedZapData); } diff --git a/packages/contracts-rfq/test/libs/ZapDataV1.t.sol b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol index f592782ca8..6e02424d8e 100644 --- a/packages/contracts-rfq/test/libs/ZapDataV1.t.sol +++ b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol @@ -18,6 +18,8 @@ contract ZapDataV1Test is Test { function encodeZapData( uint16 version, uint16 amountPosition, + address finalToken, + address forwardTo, address target, bytes memory payload ) @@ -25,10 +27,12 @@ contract ZapDataV1Test is Test { pure returns (bytes memory) { - return abi.encodePacked(version, amountPosition, target, payload); + return abi.encodePacked(version, amountPosition, finalToken, forwardTo, target, payload); } function test_roundtrip_withAmount( + address finalToken, + address forwardTo, address target, uint256 amount, bytes memory prefix, @@ -46,37 +50,54 @@ contract ZapDataV1Test is Test { // We expect the correct amount to be substituted in the payload at the time of Zap. bytes memory finalPayload = abi.encodePacked(prefix, amount, postfix); - bytes memory zapData = harness.encodeV1(amountPosition, target, encodedPayload); + bytes memory zapData = harness.encodeV1(amountPosition, finalToken, forwardTo, target, encodedPayload); harness.validateV1(zapData); assertEq(harness.version(zapData), 1); + assertEq(harness.finalToken(zapData), finalToken); + assertEq(harness.forwardTo(zapData), forwardTo); assertEq(harness.target(zapData), target); assertEq(harness.payload(zapData, amount), finalPayload); // Check against manually encoded ZapData. - assertEq(zapData, encodeZapData(EXPECTED_VERSION, amountPosition, target, encodedPayload)); + assertEq( + zapData, encodeZapData(EXPECTED_VERSION, amountPosition, finalToken, forwardTo, target, encodedPayload) + ); } - function test_roundtrip_noAmount(address target, uint256 amount, bytes memory payload) public view { + function test_roundtrip_noAmount( + address finalToken, + address forwardTo, + address target, + uint256 amount, + bytes memory payload + ) + public + view + { vm.assume(payload.length < type(uint16).max); vm.assume(target != address(0)); uint16 amountPosition = type(uint16).max; - bytes memory zapData = harness.encodeV1(amountPosition, target, payload); + bytes memory zapData = harness.encodeV1(amountPosition, finalToken, forwardTo, target, payload); harness.validateV1(zapData); assertEq(harness.version(zapData), 1); + assertEq(harness.finalToken(zapData), finalToken); + assertEq(harness.forwardTo(zapData), forwardTo); assertEq(harness.target(zapData), target); assertEq(harness.payload(zapData, amount), payload); // Check against manually encoded ZapData. - assertEq(zapData, encodeZapData(EXPECTED_VERSION, amountPosition, target, payload)); + assertEq(zapData, encodeZapData(EXPECTED_VERSION, amountPosition, finalToken, forwardTo, target, payload)); } function test_encodeV1_revert_targetZeroAddress() public { vm.expectRevert(ZapDataV1.ZapDataV1__TargetZeroAddress.selector); - harness.encodeV1(type(uint16).max, address(0), ""); + harness.encodeV1(type(uint16).max, address(0), address(0), address(0), ""); } function test_encodeDecodeV1_revert_invalidAmountPosition( + address finalToken, + address forwardTo, address target, uint16 amountPosition, uint256 amount, @@ -90,13 +111,16 @@ contract ZapDataV1Test is Test { uint16 incorrectMin = payload.length > 31 ? uint16(payload.length) - 31 : 0; uint16 incorrectMax = type(uint16).max - 1; amountPosition = uint16(bound(uint256(amountPosition), incorrectMin, incorrectMax)); - bytes memory invalidEncodedZapData = abi.encodePacked(uint16(1), amountPosition, target, payload); + bytes memory invalidEncodedZapData = + encodeZapData(uint16(1), amountPosition, finalToken, forwardTo, target, payload); vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); - harness.encodeV1(amountPosition, target, payload); + harness.encodeV1(amountPosition, finalToken, forwardTo, target, payload); // Validation should pass harness.validateV1(invalidEncodedZapData); + harness.finalToken(invalidEncodedZapData); + harness.forwardTo(invalidEncodedZapData); harness.target(invalidEncodedZapData); // But payload extraction should revert vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); @@ -105,6 +129,8 @@ contract ZapDataV1Test is Test { function test_validateV1_revert_unsupportedVersion_withAmount( uint16 version, + address finalToken, + address forwardTo, address target, bytes memory prefix, bytes memory postfix @@ -117,7 +143,8 @@ contract ZapDataV1Test is Test { uint16 amountPosition = uint16(prefix.length); bytes memory encodedPayload = abi.encodePacked(prefix, uint256(0), postfix); - bytes memory invalidEncodedZapData = encodeZapData(version, amountPosition, target, encodedPayload); + bytes memory invalidEncodedZapData = + encodeZapData(version, amountPosition, finalToken, forwardTo, target, encodedPayload); vm.expectRevert(abi.encodeWithSelector(ZapDataV1.ZapDataV1__UnsupportedVersion.selector, version)); harness.validateV1(invalidEncodedZapData); @@ -125,6 +152,8 @@ contract ZapDataV1Test is Test { function test_validateV1_revert_unsupportedVersion_noAmount( uint16 version, + address finalToken, + address forwardTo, address target, bytes memory payload ) @@ -134,14 +163,16 @@ contract ZapDataV1Test is Test { vm.assume(payload.length < type(uint16).max); uint16 amountPosition = type(uint16).max; - bytes memory invalidEncodedZapData = encodeZapData(version, amountPosition, target, payload); + bytes memory invalidEncodedZapData = + encodeZapData(version, amountPosition, finalToken, forwardTo, target, payload); vm.expectRevert(abi.encodeWithSelector(ZapDataV1.ZapDataV1__UnsupportedVersion.selector, version)); harness.validateV1(invalidEncodedZapData); } function test_validateV1_revert_invalidLength(bytes calldata fuzzData) public { - bytes memory minimumValidZapData = encodeZapData(EXPECTED_VERSION, type(uint16).max, address(0), ""); + bytes memory minimumValidZapData = + encodeZapData(EXPECTED_VERSION, type(uint16).max, address(0), address(0), address(0), ""); uint256 invalidLength = fuzzData.length % minimumValidZapData.length; bytes calldata invalidEncodedZapData = fuzzData[:invalidLength]; diff --git a/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol b/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol index 4d8c5b6ba3..56d9fbe439 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol @@ -110,6 +110,9 @@ contract SynapseIntentPreviewerTest is Test { function getSwapZapData(uint8 indexIn, uint8 indexOut) public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: defaultPoolMock, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) payload_: abi.encodeCall(DefaultPoolMock.swap, (indexIn, indexOut, 0, 0, type(uint256).max)), // Amount (dx) is encoded as the third parameter @@ -153,6 +156,9 @@ contract SynapseIntentPreviewerTest is Test { uint256[] memory amounts = new uint256[](TOKENS); return zapDataLib.encodeV1({ target_: defaultPoolMock, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // addLiquidity(amounts, minToMint, deadline) payload_: abi.encodeCall(IDefaultExtendedPool.addLiquidity, (amounts, 0, type(uint256).max)), // Amount is encoded within `amounts` at `TOKEN_IN_INDEX`, `amounts` is encoded after @@ -196,6 +202,9 @@ contract SynapseIntentPreviewerTest is Test { function getRemoveLiquidityZapData(uint8 indexOut) public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: defaultPoolMock, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) payload_: abi.encodeCall(IDefaultExtendedPool.removeLiquidityOneToken, (0, indexOut, 0, type(uint256).max)), // Amount (tokenAmount) is encoded as the first parameter @@ -235,6 +244,9 @@ contract SynapseIntentPreviewerTest is Test { function getWrapETHZapData() public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: weth, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // deposit() payload_: abi.encodeCall(WETHMock.deposit, ()), // Amount is not encoded @@ -269,6 +281,9 @@ contract SynapseIntentPreviewerTest is Test { function getUnwrapWETHZapData() public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: weth, + // TODO + finalToken_: address(0), + forwardTo_: address(0), // withdraw(amount) payload_: abi.encodeCall(WETHMock.withdraw, (0)), // Amount is encoded as the first parameter diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol index e081831372..9f407eec17 100644 --- a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol @@ -375,7 +375,7 @@ contract TokenZapV1Test is Test { function getZeroTargetZapData(bytes memory payload, uint16 amountPosition) public pure returns (bytes memory) { // Encode manually as the library checks for zero address - return abi.encodePacked(ZapDataV1.VERSION, amountPosition, address(0), payload); + return abi.encodePacked(ZapDataV1.VERSION, amountPosition, address(0), address(0), address(0), payload); } function test_zap_erc20_revert_notEnoughTokens() public { From 535b4e7c6088845afe2a23ccba966def29f113b9 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:06:06 +0000 Subject: [PATCH 02/12] feat: add finalToken, forwardTo --- .../contracts/libs/ZapDataV1.sol | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/contracts-rfq/contracts/libs/ZapDataV1.sol b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol index ceadb37890..37c172eed2 100644 --- a/packages/contracts-rfq/contracts/libs/ZapDataV1.sol +++ b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol @@ -12,13 +12,17 @@ library ZapDataV1 { // Offsets of the fields in the packed ZapData struct // uint16 version [000 .. 002) // uint16 amountPosition [002 .. 004) - // address target [004 .. 024) - // bytes payload [024 .. ***) + // address finalToken [004 .. 024) + // address forwardTo [024 .. 044) + // address target [044 .. 064) + // bytes payload [064 .. ***) // forgefmt: disable-start uint256 private constant OFFSET_AMOUNT_POSITION = 2; - uint256 private constant OFFSET_TARGET = 4; - uint256 private constant OFFSET_PAYLOAD = 24; + uint256 private constant OFFSET_FINAL_TOKEN = 4; + uint256 private constant OFFSET_FORWARD_TO = 24; + uint256 private constant OFFSET_TARGET = 44; + uint256 private constant OFFSET_PAYLOAD = 64; // forgefmt: disable-end error ZapDataV1__InvalidEncoding(); @@ -73,7 +77,7 @@ library ZapDataV1 { if (amountPosition_ != AMOUNT_NOT_PRESENT && (uint256(amountPosition_) + 32 > payload_.length)) { revert ZapDataV1__InvalidEncoding(); } - return abi.encodePacked(VERSION, amountPosition_, target_, payload_); + return abi.encodePacked(VERSION, amountPosition_, finalToken_, forwardTo_, target_, payload_); } /// @notice Extracts the version from the encoded Zap Data. @@ -86,12 +90,18 @@ library ZapDataV1 { /// @notice Extracts the finalToken address from the encoded Zap Data. function finalToken(bytes calldata encodedZapData) internal pure returns (address finalToken_) { - // TODO + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + finalToken_ := shr(96, calldataload(add(encodedZapData.offset, OFFSET_FINAL_TOKEN))) + } } /// @notice Extracts the forwardTo address from the encoded Zap Data. function forwardTo(bytes calldata encodedZapData) internal pure returns (address forwardTo_) { - // TODO + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + forwardTo_ := shr(96, calldataload(add(encodedZapData.offset, OFFSET_FORWARD_TO))) + } } /// @notice Extracts the target address from the encoded Zap Data. From 1a37090913600a634281b970f252ed5f422bc464 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:08:37 +0000 Subject: [PATCH 03/12] feat: scaffold TokenZap.encodeZapData --- .../contracts/zaps/TokenZapV1.sol | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol index 697dd4e327..ecf47ddb1e 100644 --- a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -28,6 +28,7 @@ contract TokenZapV1 is IZapRecipient { error TokenZapV1__PayloadLengthAboveMax(); error TokenZapV1__TargetZeroAddress(); + error TokenZapV1__TokenZeroAddress(); /// @notice Allows the contract to receive ETH. /// @dev Leftover ETH can be claimed by anyone. Ensure the full balance is spent during Zaps. @@ -100,10 +101,20 @@ contract TokenZapV1 is IZapRecipient { /// the list of parameters of the target function (starting from 0). /// Any value greater than or equal to `payload.length` can be used if the token amount is /// not an argument of the target function. + /// @param finalToken The token produced as a result of the Zap action (ERC20 or native gas token). + /// A zero address value signals that the Zap action doesn't result in any asset per se, + /// like bridging or depositing into a vault without an LP token. + /// Note: this parameter must be set to a non-zero value if the `forwardTo` parameter is + /// set to a non-zero value. + /// @param forwardTo The address to which `finalToken` should be forwarded. This parameter is required only + /// if the Zap action does not automatically transfer the token to the intended recipient. + /// Otherwise, it must be set to address(0). function encodeZapData( address target, bytes memory payload, - uint256 amountPosition + uint256 amountPosition, + address finalToken, + address forwardTo ) external pure @@ -119,8 +130,13 @@ contract TokenZapV1 is IZapRecipient { amountPosition = ZapDataV1.AMOUNT_NOT_PRESENT; } // At this point, we have checked that both `amountPosition` and `payload.length` fit in uint16. - // TODO - return ZapDataV1.encodeV1(uint16(amountPosition), address(0), address(0), target, payload); + return ZapDataV1.encodeV1({ + amountPosition_: uint16(amountPosition), + finalToken_: finalToken, + forwardTo_: forwardTo, + target_: target, + payload_: payload + }); } /// @notice Decodes the ZapData for a Zap action. Replaces the placeholder amount with the actual amount, From 52ba76024fd9d39de0e035383919dbcf924b5380 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 7 Dec 2024 17:54:28 +0000 Subject: [PATCH 04/12] test: update existing tests --- .../test/integration/TokenZapV1.t.sol | 20 +- .../test/router/SynapseIntentRouter.t.sol | 25 +- .../test/zaps/TokenZapV1.GasBench.t.sol | 2 +- .../contracts-rfq/test/zaps/TokenZapV1.t.sol | 292 ++++++++++-------- 4 files changed, 200 insertions(+), 139 deletions(-) diff --git a/packages/contracts-rfq/test/integration/TokenZapV1.t.sol b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol index 92d3874970..27fca30154 100644 --- a/packages/contracts-rfq/test/integration/TokenZapV1.t.sol +++ b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol @@ -84,7 +84,9 @@ abstract contract TokenZapV1IntegrationTest is Test { bytes memory zapData = dstZap.encodeZapData({ target: address(dstVault), payload: getDepositPayload(address(dstToken)), - amountPosition: 4 + 32 * 2 + amountPosition: 4 + 32 * 2, + finalToken: address(0), + forwardTo: address(0) }); depositTokenParams.zapData = zapData; depositTokenWithZapNativeParams.zapData = zapData; @@ -93,24 +95,32 @@ abstract contract TokenZapV1IntegrationTest is Test { depositNativeParams.zapData = dstZap.encodeZapData({ target: address(dstVault), payload: getDepositPayload(NATIVE_GAS_TOKEN), - amountPosition: 4 + 32 * 2 + amountPosition: 4 + 32 * 2, + finalToken: address(0), + forwardTo: address(0) }); // Deposit no amount depositNativeNoAmountParams.zapData = dstZap.encodeZapData({ target: address(dstVault), payload: getDepositNoAmountPayload(), - amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT, + finalToken: address(0), + forwardTo: address(0) }); // Deposit revert depositTokenRevertParams.zapData = dstZap.encodeZapData({ target: address(dstVault), payload: getDepositRevertPayload(), - amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT, + finalToken: address(0), + forwardTo: address(0) }); depositNativeRevertParams.zapData = dstZap.encodeZapData({ target: address(dstVault), payload: getDepositRevertPayload(), - amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT, + finalToken: address(0), + forwardTo: address(0) }); } diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol index 110be2f523..69e15254d2 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol @@ -76,26 +76,33 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { target: address(weth), payload: abi.encodeCall(weth.deposit, ()), // Amount is not encoded - amountPosition: type(uint256).max + amountPosition: type(uint256).max, + finalToken: address(weth), + forwardTo: address(0) }); } - function getUnwrapZapData() public view returns (bytes memory) { + function getUnwrapZapData(address forwardTo) public view returns (bytes memory) { return tokenZap.encodeZapData({ target: address(weth), payload: abi.encodeCall(weth.withdraw, (AMOUNT)), // Amount is encoded as the first parameter - amountPosition: 4 + amountPosition: 4, + finalToken: NATIVE_GAS_TOKEN, + forwardTo: forwardTo }); } - function getSwapZapData(address token) public view returns (bytes memory) { + function getSwapZapData(address token, address forwardTo) public view returns (bytes memory) { + address otherToken = token == address(weth) ? address(erc20) : address(weth); return tokenZap.encodeZapData({ target: address(pool), // Use placeholder zero amount payload: abi.encodeCall(pool.swap, (0, token)), // Amount is encoded as the first parameter - amountPosition: 4 + amountPosition: 4, + finalToken: otherToken, + forwardTo: forwardTo }); } @@ -105,7 +112,9 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { // Use placeholder zero amount payload: abi.encodeCall(vault.deposit, (token, 0, user)), // Amount is encoded as the second parameter - amountPosition: 4 + 32 + amountPosition: 4 + 32, + finalToken: address(0), + forwardTo: address(0) }); } @@ -433,7 +442,7 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { token: address(weth), amount: amountSwap, msgValue: 0, - zapData: getSwapZapData(address(weth)) + zapData: getSwapZapData(address(weth), address(0)) }), // deposit ERC20 ISynapseIntentRouter.StepParams({ @@ -826,7 +835,7 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { token: address(weth), amount: amountUnwrap, msgValue: 0, - zapData: getUnwrapZapData() + zapData: getUnwrapZapData(address(0)) }), // Deposit ETH ISynapseIntentRouter.StepParams({ diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol index 5352a5e4fb..9c9798c997 100644 --- a/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol @@ -42,7 +42,7 @@ contract TokenZapV1GasBenchmarkTest is Test { function getZapData(bytes memory originalPayload) public view returns (bytes memory) { // Amount is the second argument of the deposit function. - return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32); + return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32, address(0), address(0)); } function test_deposit_erc20() public { diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol index 9f407eec17..26a8fe786e 100644 --- a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol @@ -54,13 +54,13 @@ contract TokenZapV1Test is Test { return abi.encodeCall(vault.depositWithRevert, ()); } - function getZapData(bytes memory originalPayload) public view returns (bytes memory) { + function getZapDataDeposit(bytes memory originalPayload) public view returns (bytes memory) { // Amount is the third argument of the deposit function - return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32 * 2); + return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32 * 2, address(0), address(0)); } - function getZapDataNoAmount(bytes memory originalPayload) public view returns (bytes memory) { - return tokenZap.encodeZapData(address(vault), originalPayload, originalPayload.length); + function getZapDataDepositNoAmount(bytes memory originalPayload) public view returns (bytes memory) { + return tokenZap.encodeZapData(address(vault), originalPayload, originalPayload.length, address(0), address(0)); } function checkERC20HappyPath(bytes memory zapData, uint256 msgValue) public { @@ -73,25 +73,25 @@ contract TokenZapV1Test is Test { } function test_zap_erc20_placeholderZero() public { - bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(address(erc20), 0)); checkERC20HappyPath(zapData, 0); } function test_zap_erc20_placeholderNonZero() public { // Use the approximate amount of tokens as placeholder - bytes memory zapData = getZapData(getVaultPayload(address(erc20), 1 ether)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(address(erc20), 1 ether)); checkERC20HappyPath(zapData, 0); } function test_zap_erc20_placeholderZero_withMsgValue() public { - bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(address(erc20), 0)); checkERC20HappyPath(zapData, 123_456); // Should forward the msg.value to the vault assertEq(address(vault).balance, 123_456); } function test_zap_erc20_placeholderNonZero_withMsgValue() public { - bytes memory zapData = getZapData(getVaultPayload(address(erc20), 1 ether)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(address(erc20), 1 ether)); checkERC20HappyPath(zapData, 123_456); // Should forward the msg.value to the vault assertEq(address(vault).balance, 123_456); @@ -119,18 +119,18 @@ contract TokenZapV1Test is Test { } function test_zap_native_placeholderZero() public { - bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 0)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(nativeGasToken, 0)); checkNativeHappyPath(zapData); } function test_zap_native_placeholderNonZero() public { // Use the approximate amount of tokens as placeholder - bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 1 ether)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(nativeGasToken, 1 ether)); checkNativeHappyPath(zapData); } function test_zap_native_noAmount() public { - bytes memory zapData = getZapDataNoAmount(getVaultPayloadNoAmount()); + bytes memory zapData = getZapDataDepositNoAmount(getVaultPayloadNoAmount()); checkNativeHappyPath(zapData); } @@ -157,7 +157,7 @@ contract TokenZapV1Test is Test { /// @notice Should be able to use amount lower than msg.value. function test_zap_native_msgValueHigherThanAmount() public { - bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 1 ether)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(nativeGasToken, 1 ether)); bytes4 returnValue = tokenZap.zap{value: AMOUNT + 1 wei}(nativeGasToken, AMOUNT, zapData); assertEq(returnValue, tokenZap.zap.selector); // Check that the vault registered the deposit @@ -169,7 +169,7 @@ contract TokenZapV1Test is Test { /// @notice Should be able to utilize both msg.value and existing native balance. function test_zap_native_msgValueLowerThanAmount_extraNative() public { deal(address(tokenZap), 1337); - bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 1 ether)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(nativeGasToken, 1 ether)); bytes4 returnValue = tokenZap.zap{value: AMOUNT - 1337}(nativeGasToken, AMOUNT, zapData); assertEq(returnValue, tokenZap.zap.selector); // Check that the vault registered the deposit @@ -178,16 +178,44 @@ contract TokenZapV1Test is Test { // ═════════════════════════════════════════════════ MULTIHOPS ═════════════════════════════════════════════════════ - function getZapDataWithdraw(uint256 amount) public view returns (bytes memory) { - return tokenZap.encodeZapData(address(weth), abi.encodeCall(WETHMock.withdraw, (amount)), 4); + function getZapDataUnwrap(uint256 amount) public view returns (bytes memory) { + return tokenZap.encodeZapData( + address(weth), abi.encodeCall(WETHMock.withdraw, (amount)), 4, nativeGasToken, address(0) + ); } - function test_zap_withdraw_depositNative_placeholderZero() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataDeposit = getZapDataNoAmount(getVaultPayloadNoAmount()); + function getZapDataUnwrapAndForward(uint256 amount, address forwardTo) public view returns (bytes memory) { + return tokenZap.encodeZapData({ + target: address(weth), + payload: abi.encodeCall(WETHMock.withdraw, (amount)), + amountPosition: 4, + finalToken: nativeGasToken, + forwardTo: forwardTo + }); + } + + function getZapDataWrap() public view returns (bytes memory) { + return tokenZap.encodeZapData( + address(weth), abi.encodeCall(WETHMock.deposit, ()), type(uint256).max, address(0), address(0) + ); + } + + function getZapDataWrapAndForward(address forwardTo) public view returns (bytes memory) { + return tokenZap.encodeZapData({ + target: address(weth), + payload: abi.encodeCall(WETHMock.deposit, ()), + amountPosition: type(uint256).max, + finalToken: address(0), + forwardTo: forwardTo + }); + } + + function test_zap_unwrap_depositNative_placeholderZero() public { + bytes memory zapDataUnwrap = getZapDataUnwrap(0); + bytes memory zapDataDeposit = getZapDataDepositNoAmount(getVaultPayloadNoAmount()); weth.transfer(address(tokenZap), AMOUNT); // Do two Zaps in a row - bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrap); assertEq(returnValue, tokenZap.zap.selector); returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataDeposit); assertEq(returnValue, tokenZap.zap.selector); @@ -195,13 +223,13 @@ contract TokenZapV1Test is Test { assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); } - function test_zap_withdraw_depositNative_placeholderNonZero() public { + function test_zap_unwrap_depositNative_placeholderNonZero() public { // Use the approximate amount of tokens as placeholder - bytes memory zapDataWithdraw = getZapDataWithdraw(1 ether); - bytes memory zapDataDeposit = getZapDataNoAmount(getVaultPayloadNoAmount()); + bytes memory zapDataUnwrap = getZapDataUnwrap(1 ether); + bytes memory zapDataDeposit = getZapDataDepositNoAmount(getVaultPayloadNoAmount()); weth.transfer(address(tokenZap), AMOUNT); // Do two Zaps in a row - bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrap); assertEq(returnValue, tokenZap.zap.selector); returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataDeposit); assertEq(returnValue, tokenZap.zap.selector); @@ -209,142 +237,140 @@ contract TokenZapV1Test is Test { assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); } - function test_zap_withdraw_depositNative_placeholderZero_extraTokens() public { + function test_zap_unwrap_depositNative_placeholderZero_extraFunds() public { // Transfer some extra tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_withdraw_depositNative_placeholderZero(); - } - - function test_zap_withdraw_depositNative_placeholderZero_extraNative() public { - // Transfer some extra native tokens to the zap contract deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_depositNative_placeholderZero(); + test_zap_unwrap_depositNative_placeholderZero(); } - function test_zap_withdraw_depositNative_placeholderNonZero_extraTokens() public { + function test_zap_unwrap_depositNative_placeholderNonZero_extraFunds() public { // Transfer some extra tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_withdraw_depositNative_placeholderNonZero(); - } - - function test_zap_withdraw_depositNative_placeholderNonZero_extraNative() public { - // Transfer some extra native tokens to the zap contract deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_depositNative_placeholderNonZero(); + test_zap_unwrap_depositNative_placeholderNonZero(); } - function test_zap_withdraw_transferNativeEOA_placeholderZero() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataTransfer = tokenZap.encodeZapData({target: user, payload: "", amountPosition: 0}); + function test_zap_unwrapForwardNativeEOA_placeholderZero() public { + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, user); weth.transfer(address(tokenZap), AMOUNT); - // Do two Zaps in a row - bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); - assertEq(returnValue, tokenZap.zap.selector); - returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); // Check that the user received the native tokens assertEq(user.balance, AMOUNT); } - function test_zap_withdraw_transferNativeEOA_placeholderNonZero() public { + function test_zap_unwrapForwardNativeEOA_placeholderNonZero() public { // Use the approximate amount of tokens as placeholder - bytes memory zapDataWithdraw = getZapDataWithdraw(1 ether); - bytes memory zapDataTransfer = tokenZap.encodeZapData({target: user, payload: "", amountPosition: 0}); + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, user); weth.transfer(address(tokenZap), AMOUNT); - // Do two Zaps in a row - bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); - assertEq(returnValue, tokenZap.zap.selector); - returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); // Check that the user received the native tokens assertEq(user.balance, AMOUNT); } - function test_zap_withdraw_transferNativeEOA_placeholderZero_extraTokens() public { + function test_zap_unwrapForwardNativeEOA_placeholderZero_extraFunds() public { // Transfer some extra tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_withdraw_transferNativeEOA_placeholderZero(); - } - - function test_zap_withdraw_transferNativeEOA_placeholderZero_extraNative() public { - // Transfer some extra native tokens to the zap contract deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_transferNativeEOA_placeholderZero(); - } - - function test_zap_withdraw_transferNativeEOA_placeholderNonZero_extraTokens() public { - // Transfer some extra tokens to the zap contract - weth.transfer(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_withdraw_transferNativeEOA_placeholderNonZero(); + test_zap_unwrapForwardNativeEOA_placeholderZero(); } - function test_zap_withdraw_transferNativeEOA_placeholderNonZero_extraNative() public { + function test_zap_unwrapForwardNativeEOA_placeholderNonZero_extraFunds() public { // Transfer some extra native tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_transferNativeEOA_placeholderNonZero(); + test_zap_unwrapForwardNativeEOA_placeholderNonZero(); } - function test_zap_withdraw_transferNativeContract_placeholderZero() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataTransfer = tokenZap.encodeZapData({target: payableMock, payload: "", amountPosition: 0}); + function test_zap_unwrapForwardNativeContract_placeholderZero() public { + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, payableMock); weth.transfer(address(tokenZap), AMOUNT); - // Do two Zaps in a row - bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); - assertEq(returnValue, tokenZap.zap.selector); - returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); // Check that the contract received the native tokens assertEq(payableMock.balance, AMOUNT); } - function test_zap_withdraw_transferNativeContract_placeholderNonZero() public { + function test_zap_unwrapForwardNativeContract_placeholderNonZero() public { // Use the approximate amount of tokens as placeholder - bytes memory zapDataWithdraw = getZapDataWithdraw(1 ether); - bytes memory zapDataTransfer = tokenZap.encodeZapData({target: payableMock, payload: "", amountPosition: 0}); + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, payableMock); weth.transfer(address(tokenZap), AMOUNT); - // Do two Zaps in a row - bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); - assertEq(returnValue, tokenZap.zap.selector); - returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); // Check that the contract received the native tokens assertEq(payableMock.balance, AMOUNT); } - function test_zap_withdraw_transferNativeContract_placeholderZero_extraTokens() public { + function test_zap_unwrapForwardNativeContract_placeholderZero_extraFunds() public { // Transfer some extra tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); + deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_transferNativeContract_placeholderZero(); + test_zap_unwrapForwardNativeContract_placeholderZero(); } - function test_zap_withdraw_transferNativeContract_placeholderZero_extraNative() public { + function test_zap_unwrapForwardNativeContract_placeholderNonZero_extraFunds() public { // Transfer some extra native tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_transferNativeContract_placeholderZero(); + test_zap_unwrapForwardNativeContract_placeholderNonZero(); + } + + function test_zap_wrap_depositWETH_placeholderZero() public { + bytes memory zapDataWrap = getZapDataWrap(); + bytes memory zapDataDeposit = getZapDataDeposit(getVaultPayload(address(weth), 0)); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapDataWrap); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataDeposit); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); } - function test_zap_withdraw_transferNativeContract_placeholderNonZero_extraTokens() public { + function test_zap_wrap_depositWETH_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapDataWrap = getZapDataWrap(); + bytes memory zapDataDeposit = getZapDataDeposit(getVaultPayload(address(weth), 1 ether)); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapDataWrap); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataDeposit); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); + } + + function test_zap_wrap_depositWETH_placeholderZero_extraFunds() public { // Transfer some extra tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); + deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_transferNativeContract_placeholderNonZero(); + test_zap_wrap_depositWETH_placeholderZero(); } - function test_zap_withdraw_transferNativeContract_placeholderNonZero_extraNative() public { + function test_zap_wrap_depositWETH_placeholderNonZero_extraFunds() public { // Transfer some extra native tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); deal(address(tokenZap), AMOUNT); // Should not affect the zap - test_zap_withdraw_transferNativeContract_placeholderNonZero(); + test_zap_wrap_depositWETH_placeholderNonZero(); + } + + function test_zap_wrapForward() public { + bytes memory zapDataWrapAndForward = getZapDataWrapAndForward(user); + weth.transfer(address(tokenZap), AMOUNT); + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapDataWrapAndForward); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the user received WETH + assertEq(weth.balanceOf(user), AMOUNT); } // ═════════════════════════════════════════════════ ENCODING ══════════════════════════════════════════════════════ @@ -353,7 +379,7 @@ contract TokenZapV1Test is Test { bytes memory originalPayload = getVaultPayload(token, placeholderAmount); bytes memory expectedPayload = getVaultPayload(token, amount); - bytes memory zapData = getZapData(originalPayload); + bytes memory zapData = getZapDataDeposit(originalPayload); (address target, bytes memory payload) = tokenZap.decodeZapData(zapData, amount); assertEq(target, address(vault)); @@ -365,7 +391,7 @@ contract TokenZapV1Test is Test { // Any value >= payload.length could be used to signal that the amount is not an argument of the target function amountPosition = bound(amountPosition, payload.length, type(uint256).max); - bytes memory zapData = tokenZap.encodeZapData(address(vault), payload, amountPosition); + bytes memory zapData = tokenZap.encodeZapData(address(vault), payload, amountPosition, address(0), address(0)); (address target, bytes memory decodedPayload) = tokenZap.decodeZapData(zapData, 0); assertEq(target, address(vault)); assertEq(decodedPayload, payload); @@ -379,7 +405,7 @@ contract TokenZapV1Test is Test { } function test_zap_erc20_revert_notEnoughTokens() public { - bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + bytes memory zapData = getZapDataDeposit(getVaultPayload(address(erc20), 0)); // Transfer tokens to the zap contract first, but not enough erc20.transfer(address(tokenZap), AMOUNT - 1); vm.expectRevert(); @@ -387,7 +413,7 @@ contract TokenZapV1Test is Test { } function test_zap_erc20_revert_targetReverted() public { - bytes memory zapData = getZapData(getVaultPayloadWithRevert()); + bytes memory zapData = getZapDataDeposit(getVaultPayloadWithRevert()); // Transfer tokens to the zap contract first erc20.transfer(address(tokenZap), AMOUNT); vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); @@ -415,7 +441,9 @@ contract TokenZapV1Test is Test { bytes memory zapData = tokenZap.encodeZapData({ target: user, payload: getVaultPayload(address(erc20), 0), - amountPosition: 4 + 32 * 2 + amountPosition: 4 + 32 * 2, + finalToken: address(0), + forwardTo: address(0) }); // Transfer tokens to the zap contract first erc20.transfer(address(tokenZap), AMOUNT); @@ -424,7 +452,13 @@ contract TokenZapV1Test is Test { } function test_zap_erc20_revert_targetEOA_emptyPayload() public { - bytes memory zapData = tokenZap.encodeZapData({target: user, payload: "", amountPosition: 0}); + bytes memory zapData = tokenZap.encodeZapData({ + target: user, + payload: "", + amountPosition: 0, + finalToken: address(0), + forwardTo: address(0) + }); // Transfer tokens to the zap contract first erc20.transfer(address(tokenZap), AMOUNT); vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, user)); @@ -432,67 +466,75 @@ contract TokenZapV1Test is Test { } function test_zap_native_revert_targetReverted() public { - bytes memory zapData = getZapData(getVaultPayloadWithRevert()); + bytes memory zapData = getZapDataDeposit(getVaultPayloadWithRevert()); vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); } function test_zap_native_revert_msgValueLowerThanExpected() public { bytes memory originalPayload = getVaultPayload(nativeGasToken, 0); - bytes memory zapData = getZapData(originalPayload); + bytes memory zapData = getZapDataDeposit(originalPayload); vm.expectRevert(abi.encodeWithSelector(Address.AddressInsufficientBalance.selector, tokenZap)); tokenZap.zap{value: 1 ether - 1 wei}(nativeGasToken, 1 ether, zapData); } - function test_zap_withdraw_transferNative_revert_targetReverted() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataTransfer = tokenZap.encodeZapData({target: nonPayableMock, payload: "", amountPosition: 0}); + function test_zap_unwrapForwardNative_revert_targetReverted() public { + bytes memory zapDataWithdrawAndForward = getZapDataUnwrapAndForward(0, nonPayableMock); weth.transfer(address(tokenZap), AMOUNT); - tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); vm.expectRevert(Address.FailedInnerCall.selector); - tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); + tokenZap.zap(address(weth), AMOUNT, zapDataWithdrawAndForward); } - function test_zap_withdraw_transferNative_revert_targetZeroAddress_emptyPayload() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataTransfer = getZeroTargetZapData({payload: "", amountPosition: 0}); - weth.transfer(address(tokenZap), AMOUNT); - tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + function test_zap_native_revert_targetZeroAddress_emptyPayload() public { + bytes memory zapData = getZeroTargetZapData({payload: "", amountPosition: 0}); vm.expectRevert(TokenZapV1.TokenZapV1__TargetZeroAddress.selector); - tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); } - function test_zap_withdraw_transferNative_revert_targetZeroAddress_nonEmptyPayload() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory payload = getVaultPayloadNoAmount(); - bytes memory zapDataTransfer = getZeroTargetZapData({payload: payload, amountPosition: uint16(payload.length)}); - weth.transfer(address(tokenZap), AMOUNT); - tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + function test_zap_native_revert_targetZeroAddress_nonEmptyPayload() public { + bytes memory zapData = getZeroTargetZapData({payload: getVaultPayloadNoAmount(), amountPosition: 0}); vm.expectRevert(TokenZapV1.TokenZapV1__TargetZeroAddress.selector); - tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); } - function test_zap_withdraw_transferNative_revert_targetEOA_nonEmptyPayload() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataTransfer = - tokenZap.encodeZapData({target: user, payload: getVaultPayloadNoAmount(), amountPosition: 0}); + function test_zap_unwrap_transferNative_revert_targetEOA_nonEmptyPayload() public { + bytes memory zapDataUnwrap = getZapDataUnwrap(0); + bytes memory zapDataTransfer = tokenZap.encodeZapData({ + target: user, + payload: getVaultPayloadNoAmount(), + amountPosition: 0, + finalToken: address(0), + forwardTo: address(0) + }); weth.transfer(address(tokenZap), AMOUNT); - tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + tokenZap.zap(address(weth), AMOUNT, zapDataUnwrap); vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, user)); tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); } + function test_zap_revert_tokenZeroAddress() public { + bytes memory zapData = getZapDataDepositNoAmount(getVaultPayloadNoAmount()); + vm.expectRevert(TokenZapV1.TokenZapV1__TokenZeroAddress.selector); + tokenZap.zap(address(0), AMOUNT, zapData); + } + function test_encodeZapData_revert_payloadLengthAboveMax() public { bytes memory tooLongPayload = new bytes(2 ** 16); vm.expectRevert(TokenZapV1.TokenZapV1__PayloadLengthAboveMax.selector); - tokenZap.encodeZapData(address(vault), tooLongPayload, 0); + tokenZap.encodeZapData(address(vault), tooLongPayload, 0, address(0), address(0)); } function test_encodeZapData_revert_targetZeroAddress() public { bytes memory payload = getVaultPayloadNoAmount(); vm.expectRevert(ZapDataV1.ZapDataV1__TargetZeroAddress.selector); - tokenZap.encodeZapData(address(0), payload, payload.length); + tokenZap.encodeZapData(address(0), payload, payload.length, address(0), address(0)); + } + + function test_encodeZapData_revert_finalTokenZeroAddressWithForwardTo() public { + bytes memory payload = getVaultPayloadNoAmount(); + vm.expectRevert(TokenZapV1.TokenZapV1__TokenZeroAddress.selector); + tokenZap.encodeZapData(address(vault), payload, payload.length, address(0), user); } } From 08a5883d8c676444e8446458d2186dd06d048f20 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:10:51 +0000 Subject: [PATCH 05/12] test: forwardTo scenarios --- .../SynapseIntentRouter.BalanceChecks.t.sol | 51 +++ .../test/router/SynapseIntentRouter.t.sol | 335 ++++++++++++++++++ 2 files changed, 386 insertions(+) diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol index af1e6f5056..7a6ba01019 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol @@ -204,4 +204,55 @@ contract SynapseIntentRouterBalanceChecksTest is SynapseIntentRouterTest { steps: steps }); } + + function test_swapUnwrapForwardNative_exactAmounts_revert_unspentERC20() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, amountSwap); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT + 1, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + } + + function test_swapUnwrapForwardNative_exactAmounts_revert_unspentWETH() public { + uint256 amountReduced = AMOUNT / TOKEN_PRICE - 1; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, amountReduced); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountReduced, + deadline: block.timestamp, + steps: steps + }); + } + + function test_swapUnwrapForwardNative_exactAmounts_extraFunds_revert_unspentERC20() public withExtraFunds { + test_swapUnwrapForwardNative_exactAmounts_revert_unspentERC20(); + } + + function test_swapUnwrapForwardNative_exactAmounts_extraFunds_revert_unspentWETH() public withExtraFunds { + test_swapUnwrapForwardNative_exactAmounts_revert_unspentWETH(); + } + + function test_swapUnwrapForwardNative_exactAmount1_extraFunds_revertWithBalanceChecks() + public + override + withExtraFunds + { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, amountSwap); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + } } diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol index 69e15254d2..35501041d3 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol @@ -426,6 +426,341 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { checkRevertsMsgValueBelowExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT - 1}); } + // ═══════════════════════════════════════════ SWAP & FORWARD ERC20 ════════════════════════════════════════════════ + + function getSwapForwardERC20Steps(uint256 amountSwap) + public + view + returns (ISynapseIntentRouter.StepParams[] memory) + { + return toArray( + // WETH -> ERC20 + ISynapseIntentRouter.StepParams({ + token: address(weth), + amount: amountSwap, + msgValue: 0, + zapData: getSwapZapData(address(weth), user) + }) + ); + } + + function test_swapForwardERC20_exactAmount() public { + uint256 initialBalance = erc20.balanceOf(user); + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(AMOUNT); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check the user erc20 balance + assertEq(erc20.balanceOf(user), initialBalance + AMOUNT * TOKEN_PRICE); + } + + /// @notice Extra funds should be used with "forward" instructions. + function test_swapForwardERC20_exactAmount_extraFunds() public withExtraFunds { + uint256 initialBalance = erc20.balanceOf(user); + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(AMOUNT); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check the user erc20 balance + assertEq(erc20.balanceOf(user), initialBalance + AMOUNT * TOKEN_PRICE + EXTRA_FUNDS); + } + + function test_swapForwardERC20_exactAmount_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(AMOUNT); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_swapForwardERC20_exactAmount_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(AMOUNT); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_swapForwardERC20_exactAmount_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(AMOUNT); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_swapForwardERC20_fullBalance() public { + uint256 initialBalance = erc20.balanceOf(user); + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check the user erc20 balance + assertEq(erc20.balanceOf(user), initialBalance + AMOUNT * TOKEN_PRICE); + } + + /// @notice Extra funds should be used with "full balance" instructions. + function test_swapForwardERC20_fullBalance_extraFunds() public withExtraFunds { + uint256 initialBalance = erc20.balanceOf(user); + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check the user erc20 balance with the extra funds + assertEq(erc20.balanceOf(user), initialBalance + (AMOUNT + EXTRA_FUNDS) * TOKEN_PRICE + EXTRA_FUNDS); + } + + function test_swapForwardERC20_fullBalance_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_swapForwardERC20_fullBalance_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_swapForwardERC20_fullBalance_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getSwapForwardERC20Steps(FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + + // ══════════════════════════════════════ SWAP & UNWRAP & FORWARD NATIVE ═══════════════════════════════════════════ + + function getSwapUnwrapForwardNativeSteps( + uint256 amountSwap, + uint256 amountUnwrap + ) + public + view + returns (ISynapseIntentRouter.StepParams[] memory) + { + return toArray( + // ERC20 -> WETH + ISynapseIntentRouter.StepParams({ + token: address(erc20), + amount: amountSwap, + msgValue: 0, + zapData: getSwapZapData(address(erc20), address(0)) + }), + // WETH -> ETH + ISynapseIntentRouter.StepParams({ + token: address(weth), + amount: amountUnwrap, + msgValue: 0, + zapData: getUnwrapZapData(user) + }) + ); + } + + function test_swapUnwrapForwardNative_exactAmounts() public { + uint256 initialBalance = user.balance; + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, amountSwap); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance + assertEq(user.balance, initialBalance + amountSwap); + } + + /// @notice Extra funds should be used with the last forward instruction. + function test_swapUnwrapForwardNative_exactAmounts_extraFunds() public withExtraFunds { + uint256 initialBalance = user.balance; + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, amountSwap); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance + assertEq(user.balance, initialBalance + amountSwap + EXTRA_FUNDS); + } + + function test_swapUnwrapForwardNative_exactAmounts_revert_deadlineExceeded() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, amountSwap); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountSwap, steps: steps}); + } + + function test_swapUnwrapForwardNative_exactAmounts_revert_lastStepAmountInsufficient() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, amountSwap); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountSwap + 1, steps: steps}); + } + + function test_swapUnwrapForwardNative_exactAmounts_revert_msgValueAboveExpected() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, amountSwap); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountSwap}); + } + + function test_swapUnwrapForwardNative_exactAmount0() public { + uint256 initialBalance = user.balance; + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance + assertEq(user.balance, initialBalance + amountSwap); + } + + /// @notice Extra funds should be used with the last "full balance" and forward instructions. + function test_swapUnwrapForwardNative_exactAmount0_extraFunds() public withExtraFunds { + uint256 initialBalance = user.balance; + uint256 amountSwapExtra = AMOUNT / TOKEN_PRICE + EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwapExtra, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance with the extra funds + assertEq(user.balance, initialBalance + amountSwapExtra + EXTRA_FUNDS); + } + + function test_swapUnwrapForwardNative_exactAmount0_revert_deadlineExceeded() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountSwap, steps: steps}); + } + + function test_swapUnwrapForwardNative_exactAmount0_revert_lastStepAmountInsufficient() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountSwap + 1, steps: steps}); + } + + function test_swapUnwrapForwardNative_exactAmount0_revert_msgValueAboveExpected() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(AMOUNT, FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountSwap}); + } + + function test_swapUnwrapForwardNative_exactAmount1() public { + uint256 initialBalance = user.balance; + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, amountSwap); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance + assertEq(user.balance, initialBalance + amountSwap); + } + + /// @notice Should succeed with extra funds if no balance checks are performed. + /// Extra funds should be used with the last forward instruction. + function test_swapUnwrapForwardNative_exactAmount1_extraFunds_revertWithBalanceChecks() + public + virtual + withExtraFunds + { + uint256 initialBalance = user.balance; + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, amountSwap); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance + assertEq(user.balance, initialBalance + amountSwap + EXTRA_FUNDS); + } + + function test_swapUnwrapForwardNative_exactAmount1_revert_deadlineExceeded() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, amountSwap); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountSwap, steps: steps}); + } + + function test_swapUnwrapForwardNative_exactAmount1_revert_lastStepAmountInsufficient() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, amountSwap); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountSwap + 1, steps: steps}); + } + + function test_swapUnwrapForwardNative_exactAmount1_revert_msgValueAboveExpected() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, amountSwap); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountSwap}); + } + + function test_swapUnwrapForwardNative_fullBalances() public { + uint256 initialBalance = user.balance; + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwap, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance + assertEq(user.balance, initialBalance + amountSwap); + } + + /// @notice Extra funds should be used with both "full balance" instructions, and with the last forward instruction. + function test_swapUnwrapForwardNative_fullBalances_extraFunds() public withExtraFunds { + uint256 initialBalance = user.balance; + uint256 amountSwapExtra = (AMOUNT + EXTRA_FUNDS) / TOKEN_PRICE + EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountSwapExtra, + deadline: block.timestamp, + steps: steps + }); + // Check the user native balance with the extra funds + assertEq(user.balance, initialBalance + amountSwapExtra + EXTRA_FUNDS); + } + + function test_swapUnwrapForwardNative_fullBalances_revert_deadlineExceeded() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountSwap, steps: steps}); + } + + function test_swapUnwrapForwardNative_fullBalances_revert_lastStepAmountInsufficient() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountSwap + 1, steps: steps}); + } + + function test_swapUnwrapForwardNative_fullBalances_revert_msgValueAboveExpected() public { + uint256 amountSwap = AMOUNT / TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapUnwrapForwardNativeSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountSwap}); + } + // ═══════════════════════════════════════════ SWAP & DEPOSIT ERC20 ════════════════════════════════════════════════ function getSwapDepositERC20Steps( From 6d7cc518aba1e0e54d8000c06712ae20593794fc Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:12:01 +0000 Subject: [PATCH 06/12] feat: token zeor address checks --- packages/contracts-rfq/contracts/zaps/TokenZapV1.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol index ecf47ddb1e..1d44b18557 100644 --- a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -47,6 +47,7 @@ contract TokenZapV1 is IZapRecipient { /// @param zapData Encoded Zap Data containing the target address and calldata for the Zap action. /// @return selector Selector of this function to signal the caller about the success of the Zap action. function zap(address token, uint256 amount, bytes calldata zapData) external payable returns (bytes4) { + if (token == address(0)) revert TokenZapV1__TokenZeroAddress(); // Validate the ZapData format and extract the target address. zapData.validateV1(); address target = zapData.target(); @@ -123,6 +124,10 @@ contract TokenZapV1 is IZapRecipient { if (payload.length > ZapDataV1.AMOUNT_NOT_PRESENT) { revert TokenZapV1__PayloadLengthAboveMax(); } + // Final token needs to be specified if forwarding is required. + if (forwardTo != address(0) && finalToken == address(0)) { + revert TokenZapV1__TokenZeroAddress(); + } // External integrations do not need to understand the specific `AMOUNT_NOT_PRESENT` semantics. // Therefore, they can specify any value greater than or equal to `payload.length` to indicate // that the amount is not present in the payload. From 26ed1e72e89d0779aec2de5ef8305aff7509cdc0 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:27:03 +0000 Subject: [PATCH 07/12] feat:scaffold token forwarding --- packages/contracts-rfq/contracts/zaps/TokenZapV1.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol index 1d44b18557..f1c209bf5d 100644 --- a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -26,6 +26,7 @@ contract TokenZapV1 is IZapRecipient { address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + error TokenZapV1__FinalTokenBalanceZero(); error TokenZapV1__PayloadLengthAboveMax(); error TokenZapV1__TargetZeroAddress(); error TokenZapV1__TokenZeroAddress(); @@ -83,6 +84,11 @@ contract TokenZapV1 is IZapRecipient { // Note: this will bubble up any revert from the target contract, and revert if target is EOA. Address.functionCallWithValue({target: target, data: payload, value: msgValue}); } + // Forward the final token to the specified recipient, if required. + address forwardTo = zapData.forwardTo(); + if (forwardTo != address(0)) { + _forwardToken(zapData.finalToken(), forwardTo); + } // Return function selector to indicate successful execution return this.zap.selector; } @@ -160,4 +166,9 @@ contract TokenZapV1 is IZapRecipient { target = zapData.target(); payload = zapData.payload(amount); } + + /// @notice Forwards the proceeds of the Zap action to the specified recipient. + function _forwardToken(address token, address forwardTo) internal { + // TODO + } } From 0c7ca8b13cdaef5240af7f24ac4fc917b5e616c4 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:57:22 +0000 Subject: [PATCH 08/12] test: more revert cases --- .../contracts-rfq/test/zaps/TokenZapV1.t.sol | 132 +++++++++++++++--- 1 file changed, 113 insertions(+), 19 deletions(-) diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol index 26a8fe786e..5842b25172 100644 --- a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol @@ -184,12 +184,20 @@ contract TokenZapV1Test is Test { ); } - function getZapDataUnwrapAndForward(uint256 amount, address forwardTo) public view returns (bytes memory) { + function getZapDataUnwrapAndForward( + uint256 amount, + address finalToken, + address forwardTo + ) + public + view + returns (bytes memory) + { return tokenZap.encodeZapData({ target: address(weth), payload: abi.encodeCall(WETHMock.withdraw, (amount)), amountPosition: 4, - finalToken: nativeGasToken, + finalToken: finalToken, forwardTo: forwardTo }); } @@ -200,12 +208,12 @@ contract TokenZapV1Test is Test { ); } - function getZapDataWrapAndForward(address forwardTo) public view returns (bytes memory) { + function getZapDataWrapAndForward(address finalToken, address forwardTo) public view returns (bytes memory) { return tokenZap.encodeZapData({ target: address(weth), payload: abi.encodeCall(WETHMock.deposit, ()), amountPosition: type(uint256).max, - finalToken: address(0), + finalToken: finalToken, forwardTo: forwardTo }); } @@ -254,7 +262,7 @@ contract TokenZapV1Test is Test { } function test_zap_unwrapForwardNativeEOA_placeholderZero() public { - bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, user); + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, nativeGasToken, user); weth.transfer(address(tokenZap), AMOUNT); bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); @@ -264,7 +272,7 @@ contract TokenZapV1Test is Test { function test_zap_unwrapForwardNativeEOA_placeholderNonZero() public { // Use the approximate amount of tokens as placeholder - bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, user); + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, nativeGasToken, user); weth.transfer(address(tokenZap), AMOUNT); bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); @@ -276,20 +284,32 @@ contract TokenZapV1Test is Test { // Transfer some extra tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); deal(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_unwrapForwardNativeEOA_placeholderZero(); + // Extra funds will be used when forwarding the proceeds + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, nativeGasToken, user); + weth.transfer(address(tokenZap), AMOUNT); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the user received the native tokens with extra funds + assertEq(weth.balanceOf(address(tokenZap)), AMOUNT); + assertEq(user.balance, 2 * AMOUNT); } function test_zap_unwrapForwardNativeEOA_placeholderNonZero_extraFunds() public { // Transfer some extra native tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); deal(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_unwrapForwardNativeEOA_placeholderNonZero(); + // Extra funds will be used when forwarding the proceeds + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, nativeGasToken, user); + weth.transfer(address(tokenZap), AMOUNT); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the user received the native tokens with extra funds + assertEq(weth.balanceOf(address(tokenZap)), AMOUNT); + assertEq(user.balance, 2 * AMOUNT); } function test_zap_unwrapForwardNativeContract_placeholderZero() public { - bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, payableMock); + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, nativeGasToken, payableMock); weth.transfer(address(tokenZap), AMOUNT); bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); @@ -299,7 +319,7 @@ contract TokenZapV1Test is Test { function test_zap_unwrapForwardNativeContract_placeholderNonZero() public { // Use the approximate amount of tokens as placeholder - bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, payableMock); + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, nativeGasToken, payableMock); weth.transfer(address(tokenZap), AMOUNT); bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); assertEq(returnValue, tokenZap.zap.selector); @@ -311,16 +331,28 @@ contract TokenZapV1Test is Test { // Transfer some extra tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); deal(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_unwrapForwardNativeContract_placeholderZero(); + // Extra funds will be used when forwarding the proceeds + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(0, nativeGasToken, payableMock); + weth.transfer(address(tokenZap), AMOUNT); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the contract received the native tokens with extra funds + assertEq(weth.balanceOf(address(tokenZap)), AMOUNT); + assertEq(payableMock.balance, 2 * AMOUNT); } function test_zap_unwrapForwardNativeContract_placeholderNonZero_extraFunds() public { // Transfer some extra native tokens to the zap contract weth.transfer(address(tokenZap), AMOUNT); deal(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_unwrapForwardNativeContract_placeholderNonZero(); + // Extra funds will be used when forwarding the proceeds + bytes memory zapDataUnwrapAndForward = getZapDataUnwrapAndForward(1 ether, nativeGasToken, payableMock); + weth.transfer(address(tokenZap), AMOUNT); + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataUnwrapAndForward); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the contract received the native tokens with extra funds + assertEq(weth.balanceOf(address(tokenZap)), AMOUNT); + assertEq(payableMock.balance, 2 * AMOUNT); } function test_zap_wrap_depositWETH_placeholderZero() public { @@ -365,14 +397,26 @@ contract TokenZapV1Test is Test { } function test_zap_wrapForward() public { - bytes memory zapDataWrapAndForward = getZapDataWrapAndForward(user); - weth.transfer(address(tokenZap), AMOUNT); + bytes memory zapDataWrapAndForward = getZapDataWrapAndForward(address(weth), user); bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapDataWrapAndForward); assertEq(returnValue, tokenZap.zap.selector); // Check that the user received WETH assertEq(weth.balanceOf(user), AMOUNT); } + function test_zap_wrapForward_extraFunds() public { + // Transfer some extra tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); + deal(address(tokenZap), AMOUNT); + // Extra funds will be used when forwarding the proceeds + bytes memory zapDataWrapAndForward = getZapDataWrapAndForward(address(weth), user); + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapDataWrapAndForward); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the user received WETH with extra funds + assertEq(address(tokenZap).balance, AMOUNT); + assertEq(weth.balanceOf(user), 2 * AMOUNT); + } + // ═════════════════════════════════════════════════ ENCODING ══════════════════════════════════════════════════════ function test_encodeZapData_roundtrip(address token, uint256 placeholderAmount, uint256 amount) public view { @@ -404,6 +448,20 @@ contract TokenZapV1Test is Test { return abi.encodePacked(ZapDataV1.VERSION, amountPosition, address(0), address(0), address(0), payload); } + function getZeroFinalTokenZapData( + bytes memory payload, + uint16 amountPosition, + address target, + address forwardTo + ) + public + pure + returns (bytes memory) + { + // Encode manually as the library checks for zero address + return abi.encodePacked(ZapDataV1.VERSION, amountPosition, address(0), forwardTo, target, payload); + } + function test_zap_erc20_revert_notEnoughTokens() public { bytes memory zapData = getZapDataDeposit(getVaultPayload(address(erc20), 0)); // Transfer tokens to the zap contract first, but not enough @@ -480,7 +538,7 @@ contract TokenZapV1Test is Test { } function test_zap_unwrapForwardNative_revert_targetReverted() public { - bytes memory zapDataWithdrawAndForward = getZapDataUnwrapAndForward(0, nonPayableMock); + bytes memory zapDataWithdrawAndForward = getZapDataUnwrapAndForward(0, nativeGasToken, nonPayableMock); weth.transfer(address(tokenZap), AMOUNT); vm.expectRevert(Address.FailedInnerCall.selector); tokenZap.zap(address(weth), AMOUNT, zapDataWithdrawAndForward); @@ -498,6 +556,42 @@ contract TokenZapV1Test is Test { tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); } + function test_zap_wrapForward_revert_zeroFinalToken() public { + bytes memory zapData = getZeroFinalTokenZapData({ + payload: abi.encodeCall(WETHMock.deposit, ()), + amountPosition: type(uint16).max, + target: address(weth), + forwardTo: user + }); + vm.expectRevert(TokenZapV1.TokenZapV1__TokenZeroAddress.selector); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + } + + function test_zap_wrapForward_revert_incorrectFinalToken() public { + bytes memory zapData = getZapDataWrapAndForward(nativeGasToken, user); + vm.expectRevert(TokenZapV1.TokenZapV1__FinalTokenBalanceZero.selector); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + } + + function test_zap_unwrapForward_revert_zeroFinalToken() public { + bytes memory zapData = getZeroFinalTokenZapData({ + payload: abi.encodeCall(WETHMock.withdraw, (0)), + amountPosition: 4, + target: address(weth), + forwardTo: user + }); + weth.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(TokenZapV1.TokenZapV1__TokenZeroAddress.selector); + tokenZap.zap(address(weth), AMOUNT, zapData); + } + + function test_zap_unwrapForward_revert_incorrectFinalToken() public { + bytes memory zapData = getZapDataUnwrapAndForward(0, address(weth), user); + weth.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(TokenZapV1.TokenZapV1__FinalTokenBalanceZero.selector); + tokenZap.zap(address(weth), AMOUNT, zapData); + } + function test_zap_unwrap_transferNative_revert_targetEOA_nonEmptyPayload() public { bytes memory zapDataUnwrap = getZapDataUnwrap(0); bytes memory zapDataTransfer = tokenZap.encodeZapData({ From 812a106b8e32d60a2a4a9c98898a752a216dcc51 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:58:35 +0000 Subject: [PATCH 09/12] feat: final token forwarding --- .../contracts-rfq/contracts/zaps/TokenZapV1.sol | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol index f1c209bf5d..0e0e7859dc 100644 --- a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -167,8 +167,17 @@ contract TokenZapV1 is IZapRecipient { payload = zapData.payload(amount); } - /// @notice Forwards the proceeds of the Zap action to the specified recipient. + /// @notice Forwards the proceeds of the Zap action to the specified non-zero recipient. function _forwardToken(address token, address forwardTo) internal { - // TODO + // Check the token address and its balance to be safely forwarded. + if (token == address(0)) revert TokenZapV1__TokenZeroAddress(); + uint256 amount = token == NATIVE_GAS_TOKEN ? address(this).balance : IERC20(token).balanceOf(address(this)); + if (amount == 0) revert TokenZapV1__FinalTokenBalanceZero(); + // Forward the full balance of the final token to the specified recipient. + if (token == NATIVE_GAS_TOKEN) { + Address.sendValue({recipient: payable(forwardTo), amount: amount}); + } else { + IERC20(token).safeTransfer(forwardTo, amount); + } } } From 0821a511e6fe38935e35fcf5274ce4496e4d0da3 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:46:18 +0000 Subject: [PATCH 10/12] test: forwardTo behaviour in SIP --- .../interfaces/ISynapseIntentPreviewer.sol | 3 + .../router/SynapseIntentPreviewer.sol | 2 + .../test/router/SynapseIntentPreviewer.t.sol | 313 +++++++++++++----- 3 files changed, 244 insertions(+), 74 deletions(-) diff --git a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol index 9643a50f7d..f717d036d6 100644 --- a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol +++ b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol @@ -8,6 +8,8 @@ interface ISynapseIntentPreviewer { /// @dev Will not revert if the intent cannot be completed, returns empty values instead. /// @dev Returns (amountIn, []) if the intent is a no-op (tokenIn == tokenOut). /// @param swapQuoter Peripheral contract to use for swap quoting + /// @param forwardTo The address to which the proceeds of the intent should be forwarded to. + /// Note: if no forwarding is required (or done within the intent), use address(0). /// @param tokenIn Initial token for the intent /// @param tokenOut Final token for the intent /// @param amountIn Initial amount of tokens to use for the intent @@ -16,6 +18,7 @@ interface ISynapseIntentPreviewer { /// Empty if the intent cannot be completed, or if intent is a no-op (tokenIn == tokenOut). function previewIntent( address swapQuoter, + address forwardTo, address tokenIn, address tokenOut, uint256 amountIn diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol index 0d0a3e5a69..8ddc8f7964 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol @@ -21,6 +21,7 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { /// @dev Amount value that signals that the Zap step should be performed using the full ZapRecipient balance. uint256 internal constant FULL_BALANCE = type(uint256).max; + error SIP__NoOpForwardNotSupported(); error SIP__PoolTokenMismatch(); error SIP__PoolZeroAddress(); error SIP__RawParamsEmpty(); @@ -29,6 +30,7 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { /// @inheritdoc ISynapseIntentPreviewer function previewIntent( address swapQuoter, + address forwardTo, address tokenIn, address tokenOut, uint256 amountIn diff --git a/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol b/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol index 56d9fbe439..19b431b14b 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol @@ -40,6 +40,7 @@ contract SynapseIntentPreviewerTest is Test { address internal lpToken; address internal routerAdapterMock = makeAddr("Router Adapter Mock"); + address internal user = makeAddr("User"); function setUp() public { sip = new SynapseIntentPreviewer(); @@ -103,16 +104,15 @@ contract SynapseIntentPreviewerTest is Test { }); } - function getSwapZapData() public view returns (bytes memory) { - return getSwapZapData(TOKEN_IN_INDEX, TOKEN_OUT_INDEX); + function getSwapZapData(address forwardTo) public view returns (bytes memory) { + return getSwapZapData(TOKEN_IN_INDEX, TOKEN_OUT_INDEX, forwardTo); } - function getSwapZapData(uint8 indexIn, uint8 indexOut) public view returns (bytes memory) { + function getSwapZapData(uint8 indexIn, uint8 indexOut, address forwardTo) public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: defaultPoolMock, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: DefaultPoolMock(defaultPoolMock).getToken(indexOut), + forwardTo_: forwardTo, // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) payload_: abi.encodeCall(DefaultPoolMock.swap, (indexIn, indexOut, 0, 0, type(uint256).max)), // Amount (dx) is encoded as the third parameter @@ -120,17 +120,26 @@ contract SynapseIntentPreviewerTest is Test { }); } - function test_getSwapZapData() public view { + function checkSwapZapData(address forwardTo) public view { for (uint8 i = 0; i < TOKENS; i++) { for (uint8 j = 0; j < TOKENS; j++) { - bytes memory zapData = getSwapZapData(i, j); + bytes memory zapData = getSwapZapData(i, j, forwardTo); bytes memory payload = zapDataLib.payload(zapData, AMOUNT_IN); // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) assertEq(payload, abi.encodeCall(DefaultPoolMock.swap, (i, j, AMOUNT_IN, 0, type(uint256).max))); + assertEq(zapDataLib.forwardTo(zapData), forwardTo); } } } + function test_getSwapZapData_noForward() public view { + checkSwapZapData(address(0)); + } + + function test_getSwapZapData_withForward() public view { + checkSwapZapData(user); + } + function getAddLiquidityQuery(address tokenOut) public view returns (SwapQuery memory) { return SwapQuery({ routerAdapter: routerAdapterMock, @@ -148,17 +157,16 @@ contract SynapseIntentPreviewerTest is Test { }); } - function getAddLiquidityZapData() public view returns (bytes memory) { - return getAddLiquidityZapData(TOKEN_IN_INDEX); + function getAddLiquidityZapData(address forwardTo) public view returns (bytes memory) { + return getAddLiquidityZapData(TOKEN_IN_INDEX, forwardTo); } - function getAddLiquidityZapData(uint8 indexIn) public view returns (bytes memory) { + function getAddLiquidityZapData(uint8 indexIn, address forwardTo) public view returns (bytes memory) { uint256[] memory amounts = new uint256[](TOKENS); return zapDataLib.encodeV1({ target_: defaultPoolMock, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: lpToken, + forwardTo_: forwardTo, // addLiquidity(amounts, minToMint, deadline) payload_: abi.encodeCall(IDefaultExtendedPool.addLiquidity, (amounts, 0, type(uint256).max)), // Amount is encoded within `amounts` at `TOKEN_IN_INDEX`, `amounts` is encoded after @@ -167,17 +175,26 @@ contract SynapseIntentPreviewerTest is Test { }); } - function test_getAddLiquidityZapData() public view { + function checkAddLiquidityZapData(address forwardTo) public view { for (uint8 i = 0; i < TOKENS; i++) { - bytes memory zapData = getAddLiquidityZapData(i); + bytes memory zapData = getAddLiquidityZapData(i, forwardTo); bytes memory payload = zapDataLib.payload(zapData, AMOUNT_IN); uint256[] memory amounts = new uint256[](TOKENS); amounts[i] = AMOUNT_IN; // addLiquidity(amounts, minToMint, deadline) assertEq(payload, abi.encodeCall(IDefaultExtendedPool.addLiquidity, (amounts, 0, type(uint256).max))); + assertEq(zapDataLib.forwardTo(zapData), forwardTo); } } + function test_getAddLiquidityZapData_noForward() public view { + checkAddLiquidityZapData(address(0)); + } + + function test_getAddLiquidityZapData_withForward() public view { + checkAddLiquidityZapData(user); + } + function getRemoveLiquidityQuery(address tokenOut) public view returns (SwapQuery memory) { return SwapQuery({ routerAdapter: routerAdapterMock, @@ -195,16 +212,15 @@ contract SynapseIntentPreviewerTest is Test { }); } - function getRemoveLiquidityZapData() public view returns (bytes memory) { - return getRemoveLiquidityZapData(TOKEN_OUT_INDEX); + function getRemoveLiquidityZapData(address forwardTo) public view returns (bytes memory) { + return getRemoveLiquidityZapData(TOKEN_OUT_INDEX, forwardTo); } - function getRemoveLiquidityZapData(uint8 indexOut) public view returns (bytes memory) { + function getRemoveLiquidityZapData(uint8 indexOut, address forwardTo) public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: defaultPoolMock, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: DefaultPoolMock(defaultPoolMock).getToken(indexOut), + forwardTo_: forwardTo, // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) payload_: abi.encodeCall(IDefaultExtendedPool.removeLiquidityOneToken, (0, indexOut, 0, type(uint256).max)), // Amount (tokenAmount) is encoded as the first parameter @@ -212,18 +228,27 @@ contract SynapseIntentPreviewerTest is Test { }); } - function test_getRemoveLiquidityZapData() public view { + function checkRemoveLiquidityZapData(address forwardTo) public view { for (uint8 i = 0; i < TOKENS; i++) { - bytes memory zapData = getRemoveLiquidityZapData(i); + bytes memory zapData = getRemoveLiquidityZapData(i, forwardTo); bytes memory payload = zapDataLib.payload(zapData, AMOUNT_IN); // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) assertEq( payload, abi.encodeCall(IDefaultExtendedPool.removeLiquidityOneToken, (AMOUNT_IN, i, 0, type(uint256).max)) ); + assertEq(zapDataLib.forwardTo(zapData), forwardTo); } } + function test_getRemoveLiquidityZapData_noForward() public view { + checkRemoveLiquidityZapData(address(0)); + } + + function test_getRemoveLiquidityZapData_withForward() public view { + checkRemoveLiquidityZapData(user); + } + function getWrapETHQuery(address tokenOut) public view returns (SwapQuery memory) { return SwapQuery({ routerAdapter: routerAdapterMock, @@ -241,12 +266,11 @@ contract SynapseIntentPreviewerTest is Test { }); } - function getWrapETHZapData() public view returns (bytes memory) { + function getWrapETHZapData(address forwardTo) public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: weth, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: weth, + forwardTo_: forwardTo, // deposit() payload_: abi.encodeCall(WETHMock.deposit, ()), // Amount is not encoded @@ -254,11 +278,20 @@ contract SynapseIntentPreviewerTest is Test { }); } - function test_getWrapETHZapData() public view { - bytes memory zapData = getWrapETHZapData(); + function checkWrapETHZapData(address forwardTo) public view { + bytes memory zapData = getWrapETHZapData(forwardTo); bytes memory payload = zapDataLib.payload(zapData, AMOUNT_IN); // deposit() assertEq(payload, abi.encodeCall(WETHMock.deposit, ())); + assertEq(zapDataLib.forwardTo(zapData), forwardTo); + } + + function test_getWrapETHZapData_noForward() public view { + checkWrapETHZapData(address(0)); + } + + function test_getWrapETHZapData_withForward() public view { + checkWrapETHZapData(user); } function getUnwrapWETHQuery(address tokenOut) public view returns (SwapQuery memory) { @@ -278,12 +311,11 @@ contract SynapseIntentPreviewerTest is Test { }); } - function getUnwrapWETHZapData() public view returns (bytes memory) { + function getUnwrapWETHZapData(address forwardTo) public view returns (bytes memory) { return zapDataLib.encodeV1({ target_: weth, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: NATIVE_GAS_TOKEN, + forwardTo_: forwardTo, // withdraw(amount) payload_: abi.encodeCall(WETHMock.withdraw, (0)), // Amount is encoded as the first parameter @@ -291,11 +323,20 @@ contract SynapseIntentPreviewerTest is Test { }); } - function test_getUnwrapWETHZapData() public view { - bytes memory zapData = getUnwrapWETHZapData(); + function checkUnwrapWETHZapData(address forwardTo) public view { + bytes memory zapData = getUnwrapWETHZapData(forwardTo); bytes memory payload = zapDataLib.payload(zapData, AMOUNT_IN); // withdraw(amount) assertEq(payload, abi.encodeCall(WETHMock.withdraw, (AMOUNT_IN))); + assertEq(zapDataLib.forwardTo(zapData), forwardTo); + } + + function test_getUnwrapWETHZapData_noForward() public view { + checkUnwrapWETHZapData(address(0)); + } + + function test_getUnwrapWETHZapData_withForward() public view { + checkUnwrapWETHZapData(user); } function assertEq(ISynapseIntentRouter.StepParams memory a, ISynapseIntentRouter.StepParams memory b) public pure { @@ -308,16 +349,34 @@ contract SynapseIntentPreviewerTest is Test { // ════════════════════════════════════════════════ ZERO STEPS ═════════════════════════════════════════════════════ function test_previewIntent_noOp_token() public view { - (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = - sip.previewIntent({swapQuoter: swapQuoterMock, tokenIn: tokenA, tokenOut: tokenA, amountIn: AMOUNT_IN}); + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = sip.previewIntent({ + swapQuoter: swapQuoterMock, + forwardTo: address(0), + tokenIn: tokenA, + tokenOut: tokenA, + amountIn: AMOUNT_IN + }); // Checks assertEq(amountOut, AMOUNT_IN); assertEq(steps.length, 0); } + function test_previewIntent_noOp_token_revert_withForward() public { + // forwardTo is not allowed for no-op intents + vm.expectRevert(SynapseIntentPreviewer.SIP__NoOpForwardNotSupported.selector); + sip.previewIntent({ + swapQuoter: swapQuoterMock, + forwardTo: user, + tokenIn: tokenA, + tokenOut: tokenA, + amountIn: AMOUNT_IN + }); + } + function test_previewIntent_noOp_native() public view { (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = sip.previewIntent({ swapQuoter: swapQuoterMock, + forwardTo: address(0), tokenIn: NATIVE_GAS_TOKEN, tokenOut: NATIVE_GAS_TOKEN, amountIn: AMOUNT_IN @@ -327,13 +386,47 @@ contract SynapseIntentPreviewerTest is Test { assertEq(steps.length, 0); } + function test_previewIntent_noOp_native_revert_withForward() public { + // forwardTo is not allowed for no-op intents + vm.expectRevert(SynapseIntentPreviewer.SIP__NoOpForwardNotSupported.selector); + sip.previewIntent({ + swapQuoter: swapQuoterMock, + forwardTo: user, + tokenIn: NATIVE_GAS_TOKEN, + tokenOut: NATIVE_GAS_TOKEN, + amountIn: AMOUNT_IN + }); + } + function test_previewIntent_zeroAmountOut() public { // tokenOut is always populated SwapQuery memory emptyQuery; emptyQuery.tokenOut = tokenB; mockGetAmountOut({tokenIn: tokenA, tokenOut: tokenB, amountIn: AMOUNT_IN, mockQuery: emptyQuery}); - (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = - sip.previewIntent({swapQuoter: swapQuoterMock, tokenIn: tokenA, tokenOut: tokenB, amountIn: AMOUNT_IN}); + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = sip.previewIntent({ + swapQuoter: swapQuoterMock, + forwardTo: address(0), + tokenIn: tokenA, + tokenOut: tokenB, + amountIn: AMOUNT_IN + }); + // Checks + assertEq(amountOut, 0); + assertEq(steps.length, 0); + } + + function test_previewIntent_zeroAmountOut_withForward() public { + // tokenOut is always populated + SwapQuery memory emptyQuery; + emptyQuery.tokenOut = tokenB; + mockGetAmountOut({tokenIn: tokenA, tokenOut: tokenB, amountIn: AMOUNT_IN, mockQuery: emptyQuery}); + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = sip.previewIntent({ + swapQuoter: swapQuoterMock, + forwardTo: user, + tokenIn: tokenA, + tokenOut: tokenB, + amountIn: AMOUNT_IN + }); // Checks assertEq(amountOut, 0); assertEq(steps.length, 0); @@ -345,21 +438,27 @@ contract SynapseIntentPreviewerTest is Test { address tokenIn, address tokenOut, uint256 expectedAmountOut, - ISynapseIntentRouter.StepParams memory expectedStep + ISynapseIntentRouter.StepParams memory expectedStep, + address forwardTo ) public view { // Preview intent - (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = - sip.previewIntent({swapQuoter: swapQuoterMock, tokenIn: tokenIn, tokenOut: tokenOut, amountIn: AMOUNT_IN}); + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = sip.previewIntent({ + swapQuoter: swapQuoterMock, + forwardTo: forwardTo, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountIn: AMOUNT_IN + }); // Checks assertEq(amountOut, expectedAmountOut); assertEq(steps.length, 1); assertEq(steps[0], expectedStep); } - function test_previewIntent_swap() public { + function checkPreviewIntentSwap(address forwardTo) public { SwapQuery memory mockQuery = getSwapQuery(tokenB); mockGetToken(TOKEN_IN_INDEX, tokenA); mockGetToken(TOKEN_OUT_INDEX, tokenB); @@ -368,12 +467,20 @@ contract SynapseIntentPreviewerTest is Test { token: tokenA, amount: FULL_AMOUNT, msgValue: 0, - zapData: getSwapZapData() + zapData: getSwapZapData(forwardTo) }); - checkSingleStepIntent(tokenA, tokenB, SWAP_AMOUNT_OUT, expectedStep); + checkSingleStepIntent(tokenA, tokenB, SWAP_AMOUNT_OUT, expectedStep, forwardTo); } - function test_previewIntent_addLiquidity() public { + function test_previewIntent_swap() public { + checkPreviewIntentSwap(address(0)); + } + + function test_previewIntent_swap_withForward() public { + checkPreviewIntentSwap(user); + } + + function checkPreviewIntentAddLiquidity(address forwardTo) public { SwapQuery memory mockQuery = getAddLiquidityQuery(lpToken); mockGetToken(TOKEN_IN_INDEX, tokenA); mockGetAmountOut({tokenIn: tokenA, tokenOut: lpToken, amountIn: AMOUNT_IN, mockQuery: mockQuery}); @@ -381,12 +488,20 @@ contract SynapseIntentPreviewerTest is Test { token: tokenA, amount: FULL_AMOUNT, msgValue: 0, - zapData: getAddLiquidityZapData() + zapData: getAddLiquidityZapData(forwardTo) }); - checkSingleStepIntent(tokenA, lpToken, SWAP_AMOUNT_OUT, expectedStep); + checkSingleStepIntent(tokenA, lpToken, SWAP_AMOUNT_OUT, expectedStep, forwardTo); } - function test_previewIntent_removeLiquidity() public { + function test_previewIntent_addLiquidity() public { + checkPreviewIntentAddLiquidity(address(0)); + } + + function test_previewIntent_addLiquidity_withForward() public { + checkPreviewIntentAddLiquidity(user); + } + + function checkPreviewIntentRemoveLiquidity(address forwardTo) public { SwapQuery memory mockQuery = getRemoveLiquidityQuery(tokenB); mockGetToken(TOKEN_OUT_INDEX, tokenB); mockGetAmountOut({tokenIn: lpToken, tokenOut: tokenB, amountIn: AMOUNT_IN, mockQuery: mockQuery}); @@ -394,33 +509,57 @@ contract SynapseIntentPreviewerTest is Test { token: lpToken, amount: FULL_AMOUNT, msgValue: 0, - zapData: getRemoveLiquidityZapData() + zapData: getRemoveLiquidityZapData(forwardTo) }); - checkSingleStepIntent(lpToken, tokenB, SWAP_AMOUNT_OUT, expectedStep); + checkSingleStepIntent(lpToken, tokenB, SWAP_AMOUNT_OUT, expectedStep, forwardTo); } - function test_previewIntent_wrapETH() public { + function test_previewIntent_removeLiquidity() public { + checkPreviewIntentRemoveLiquidity(address(0)); + } + + function test_previewIntent_removeLiquidity_withForward() public { + checkPreviewIntentRemoveLiquidity(user); + } + + function checkPreviewIntentWrapETH(address forwardTo) public { SwapQuery memory mockQuery = getWrapETHQuery(weth); mockGetAmountOut({tokenIn: NATIVE_GAS_TOKEN, tokenOut: weth, amountIn: AMOUNT_IN, mockQuery: mockQuery}); ISynapseIntentRouter.StepParams memory expectedStep = ISynapseIntentRouter.StepParams({ token: NATIVE_GAS_TOKEN, amount: FULL_AMOUNT, msgValue: AMOUNT_IN, - zapData: getWrapETHZapData() + zapData: getWrapETHZapData(forwardTo) }); - checkSingleStepIntent(NATIVE_GAS_TOKEN, weth, AMOUNT_IN, expectedStep); + checkSingleStepIntent(NATIVE_GAS_TOKEN, weth, AMOUNT_IN, expectedStep, forwardTo); + } + + function test_previewIntent_wrapETH() public { + checkPreviewIntentWrapETH(address(0)); + } + + function test_previewIntent_wrapETH_withForward() public { + checkPreviewIntentWrapETH(user); } - function test_previewIntent_unwrapETH() public { + function checkPreviewIntentUnwrapWETH(address forwardTo) public { SwapQuery memory mockQuery = getUnwrapWETHQuery(NATIVE_GAS_TOKEN); mockGetAmountOut({tokenIn: weth, tokenOut: NATIVE_GAS_TOKEN, amountIn: AMOUNT_IN, mockQuery: mockQuery}); ISynapseIntentRouter.StepParams memory expectedStep = ISynapseIntentRouter.StepParams({ token: weth, amount: FULL_AMOUNT, msgValue: 0, - zapData: getUnwrapWETHZapData() + zapData: getUnwrapWETHZapData(forwardTo) }); - checkSingleStepIntent(weth, NATIVE_GAS_TOKEN, AMOUNT_IN, expectedStep); + checkSingleStepIntent(weth, NATIVE_GAS_TOKEN, AMOUNT_IN, expectedStep, forwardTo); + } + + function test_previewIntent_unwrapWETH() public { + checkPreviewIntentUnwrapWETH(address(0)); + } + + function test_previewIntent_unwrapWETH_withForward() public { + checkPreviewIntentUnwrapWETH(user); } // ════════════════════════════════════════════════ DOUBLE STEP ════════════════════════════════════════════════════ @@ -430,14 +569,20 @@ contract SynapseIntentPreviewerTest is Test { address tokenOut, uint256 expectedAmountOut, ISynapseIntentRouter.StepParams memory expectedStep0, - ISynapseIntentRouter.StepParams memory expectedStep1 + ISynapseIntentRouter.StepParams memory expectedStep1, + address forwardTo ) public view { // Preview intent - (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = - sip.previewIntent({swapQuoter: swapQuoterMock, tokenIn: tokenIn, tokenOut: tokenOut, amountIn: AMOUNT_IN}); + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = sip.previewIntent({ + swapQuoter: swapQuoterMock, + forwardTo: forwardTo, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountIn: AMOUNT_IN + }); // Checks assertEq(amountOut, expectedAmountOut); assertEq(steps.length, 2); @@ -445,43 +590,63 @@ contract SynapseIntentPreviewerTest is Test { assertEq(steps[1], expectedStep1); } - function test_previewIntent_swap_unwrapWETH() public { + function checkPreviewIntentSwapUnwrapWETH(address forwardTo) public { SwapQuery memory mockQuery = getSwapQuery(weth); mockGetToken(TOKEN_IN_INDEX, tokenA); mockGetToken(TOKEN_OUT_INDEX, weth); mockGetAmountOut({tokenIn: tokenA, tokenOut: NATIVE_GAS_TOKEN, amountIn: AMOUNT_IN, mockQuery: mockQuery}); - // step0: tokenA -> weth + // step0: tokenA -> weth, always no forwaring ISynapseIntentRouter.StepParams memory expectedStep0 = ISynapseIntentRouter.StepParams({ token: tokenA, amount: FULL_AMOUNT, msgValue: 0, - zapData: getSwapZapData() + zapData: getSwapZapData(address(0)) }); - // step1: weth -> NATIVE_GAS_TOKEN + // step1: weth -> NATIVE_GAS_TOKEN, optional forwarding ISynapseIntentRouter.StepParams memory expectedStep1 = ISynapseIntentRouter.StepParams({ token: weth, amount: FULL_AMOUNT, msgValue: 0, - zapData: getUnwrapWETHZapData() + zapData: getUnwrapWETHZapData(forwardTo) }); - checkDoubleStepIntent(tokenA, NATIVE_GAS_TOKEN, SWAP_AMOUNT_OUT, expectedStep0, expectedStep1); + checkDoubleStepIntent(tokenA, NATIVE_GAS_TOKEN, SWAP_AMOUNT_OUT, expectedStep0, expectedStep1, forwardTo); } - function test_previewIntent_wrapETH_swap() public { + function test_previewIntent_swapUnwrapWETH() public { + checkPreviewIntentSwapUnwrapWETH(address(0)); + } + + function test_previewIntent_swapUnwrapWETH_withForward() public { + checkPreviewIntentSwapUnwrapWETH(user); + } + + function checkPreviewIntentWrapETHSwap(address forwardTo) public { SwapQuery memory mockQuery = getSwapQuery(tokenB); mockGetToken(TOKEN_IN_INDEX, weth); mockGetToken(TOKEN_OUT_INDEX, tokenB); mockGetAmountOut({tokenIn: NATIVE_GAS_TOKEN, tokenOut: tokenB, amountIn: AMOUNT_IN, mockQuery: mockQuery}); - // step0: NATIVE_GAS_TOKEN -> weth + // step0: NATIVE_GAS_TOKEN -> weth, always no forwaring ISynapseIntentRouter.StepParams memory expectedStep0 = ISynapseIntentRouter.StepParams({ token: NATIVE_GAS_TOKEN, amount: FULL_AMOUNT, msgValue: AMOUNT_IN, - zapData: getWrapETHZapData() + zapData: getWrapETHZapData(address(0)) }); - // step1: weth -> tokenB - ISynapseIntentRouter.StepParams memory expectedStep1 = - ISynapseIntentRouter.StepParams({token: weth, amount: FULL_AMOUNT, msgValue: 0, zapData: getSwapZapData()}); - checkDoubleStepIntent(NATIVE_GAS_TOKEN, tokenB, SWAP_AMOUNT_OUT, expectedStep0, expectedStep1); + // step1: weth -> tokenB, optional forwarding + ISynapseIntentRouter.StepParams memory expectedStep1 = ISynapseIntentRouter.StepParams({ + token: weth, + amount: FULL_AMOUNT, + msgValue: 0, + zapData: getSwapZapData(forwardTo) + }); + checkDoubleStepIntent(NATIVE_GAS_TOKEN, tokenB, SWAP_AMOUNT_OUT, expectedStep0, expectedStep1, forwardTo); + } + + function test_previewIntent_wrapETHSwap() public { + checkPreviewIntentWrapETHSwap(address(0)); + } + + function test_previewIntent_wrapETHSwap_withForward() public { + checkPreviewIntentWrapETHSwap(user); } } From 2bda040d6853250a0bcfa2aab72074db22c53849 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:47:22 +0000 Subject: [PATCH 11/12] feat: support optional `forwardTo` in SIP --- .../router/SynapseIntentPreviewer.sol | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol index 8ddc8f7964..870a86c3a6 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol @@ -28,6 +28,7 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { error SIP__TokenNotNative(); /// @inheritdoc ISynapseIntentPreviewer + // solhint-disable-next-line code-complexity function previewIntent( address swapQuoter, address forwardTo, @@ -41,6 +42,7 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { { // First, check if the intent is a no-op. if (tokenIn == tokenOut) { + if (forwardTo != address(0)) revert SIP__NoOpForwardNotSupported(); return (amountIn, new ISynapseIntentRouter.StepParams[](0)); } @@ -61,13 +63,13 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { // Create the steps for the intent based on the action type. if (params.action == Action.Swap) { - steps = _createSwapSteps(tokenIn, tokenOut, amountIn, params); + steps = _createSwapSteps(tokenIn, tokenOut, amountIn, params, forwardTo); } else if (params.action == Action.AddLiquidity) { - steps = _createAddLiquiditySteps(tokenIn, tokenOut, params); + steps = _createAddLiquiditySteps(tokenIn, tokenOut, params, forwardTo); } else if (params.action == Action.RemoveLiquidity) { - steps = _createRemoveLiquiditySteps(tokenIn, tokenOut, params); + steps = _createRemoveLiquiditySteps(tokenIn, tokenOut, params, forwardTo); } else { - steps = _createHandleHativeSteps(tokenIn, tokenOut, amountIn); + steps = _createHandleHativeSteps(tokenIn, tokenOut, amountIn, forwardTo); } } @@ -76,7 +78,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { address tokenIn, address tokenOut, uint256 amountIn, - DefaultParams memory params + DefaultParams memory params, + address forwardTo ) internal view @@ -91,10 +94,10 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { address wrappedNative = IDefaultPool(pool).getToken(params.tokenIndexFrom); // Sanity check tokenOut vs tokenIndexTo. if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch(); - // Native => WrappedNative + WrappedNative => TokenOut. + // Native => WrappedNative + WrappedNative => TokenOut. Forwarding is done in the second step. return _toStepsArray( - _createWrapNativeStep({wrappedNative: wrappedNative, amountIn: amountIn}), - _createSwapStep({tokenIn: wrappedNative, params: params}) + _createWrapNativeStep({wrappedNative: wrappedNative, msgValue: amountIn, forwardTo: address(0)}), + _createSwapStep({tokenIn: wrappedNative, tokenOut: tokenOut, params: params, forwardTo: forwardTo}) ); } @@ -105,10 +108,10 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { if (tokenOut == NATIVE_GAS_TOKEN) { // Get the address of the wrapped native token. address wrappedNative = IDefaultPool(pool).getToken(params.tokenIndexTo); - // TokenIn => WrappedNative + WrappedNative => Native. + // TokenIn => WrappedNative + WrappedNative => Native. Forwarding is done in the second step. return _toStepsArray( - _createSwapStep({tokenIn: tokenIn, params: params}), - _createUnwrapNativeStep({wrappedNative: wrappedNative}) + _createSwapStep({tokenIn: tokenIn, tokenOut: wrappedNative, params: params, forwardTo: address(0)}), + _createUnwrapNativeStep({wrappedNative: wrappedNative, forwardTo: forwardTo}) ); } @@ -116,14 +119,17 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch(); // TokenIn => TokenOut. - return _toStepsArray(_createSwapStep({tokenIn: tokenIn, params: params})); + ISynapseIntentRouter.StepParams memory step = + _createSwapStep({tokenIn: tokenIn, tokenOut: tokenOut, params: params, forwardTo: forwardTo}); + return _toStepsArray(step); } /// @notice Helper function to create steps for adding liquidity. function _createAddLiquiditySteps( address tokenIn, address tokenOut, - DefaultParams memory params + DefaultParams memory params, + address forwardTo ) internal view @@ -154,9 +160,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: pool, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: tokenOut, + forwardTo_: forwardTo, // addLiquidity(amounts, minToMint, deadline) payload_: abi.encodeCall(IDefaultExtendedPool.addLiquidity, (amounts, 0, type(uint256).max)), // amountIn is encoded within `amounts` at `TOKEN_IN_INDEX`, `amounts` is encoded after @@ -171,7 +176,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { function _createRemoveLiquiditySteps( address tokenIn, address tokenOut, - DefaultParams memory params + DefaultParams memory params, + address forwardTo ) internal view @@ -190,9 +196,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: pool, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: tokenOut, + forwardTo_: forwardTo, // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) payload_: abi.encodeCall( IDefaultExtendedPool.removeLiquidityOneToken, (0, params.tokenIndexTo, 0, type(uint256).max) @@ -213,7 +218,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { function _createHandleHativeSteps( address tokenIn, address tokenOut, - uint256 amountIn + uint256 amountIn, + address forwardTo ) internal pure @@ -221,18 +227,22 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { { if (tokenIn == NATIVE_GAS_TOKEN) { // tokenOut is Wrapped Native - return _toStepsArray(_createWrapNativeStep({wrappedNative: tokenOut, amountIn: amountIn})); + return _toStepsArray( + _createWrapNativeStep({wrappedNative: tokenOut, msgValue: amountIn, forwardTo: forwardTo}) + ); } // Sanity check tokenOut if (tokenOut != NATIVE_GAS_TOKEN) revert SIP__TokenNotNative(); // tokenIn is Wrapped Native - return _toStepsArray(_createUnwrapNativeStep({wrappedNative: tokenIn})); + return _toStepsArray(_createUnwrapNativeStep({wrappedNative: tokenIn, forwardTo: forwardTo})); } /// @notice Helper function to create a single step for a swap. function _createSwapStep( address tokenIn, - DefaultParams memory params + address tokenOut, + DefaultParams memory params, + address forwardTo ) internal pure @@ -244,9 +254,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: params.pool, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: tokenOut, + forwardTo_: forwardTo, // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) payload_: abi.encodeCall( IDefaultPool.swap, (params.tokenIndexFrom, params.tokenIndexTo, 0, 0, type(uint256).max) @@ -260,7 +269,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { /// @notice Helper function to create a single step for wrapping native gas tokens. function _createWrapNativeStep( address wrappedNative, - uint256 amountIn + uint256 msgValue, + address forwardTo ) internal pure @@ -269,12 +279,11 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { return ISynapseIntentRouter.StepParams({ token: NATIVE_GAS_TOKEN, amount: FULL_BALANCE, - msgValue: amountIn, + msgValue: msgValue, zapData: ZapDataV1.encodeV1({ target_: wrappedNative, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: wrappedNative, + forwardTo_: forwardTo, // deposit() payload_: abi.encodeCall(IWETH9.deposit, ()), // amountIn is not encoded @@ -284,7 +293,10 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { } /// @notice Helper function to create a single step for unwrapping native gas tokens. - function _createUnwrapNativeStep(address wrappedNative) + function _createUnwrapNativeStep( + address wrappedNative, + address forwardTo + ) internal pure returns (ISynapseIntentRouter.StepParams memory) @@ -295,9 +307,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: wrappedNative, - // TODO - finalToken_: address(0), - forwardTo_: address(0), + finalToken_: NATIVE_GAS_TOKEN, + forwardTo_: forwardTo, // withdraw(amount) payload_: abi.encodeCall(IWETH9.withdraw, (0)), // amountIn encoded as the first parameter From 4b925f99644b5f4140f89e75aff3035980bb1aa7 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:52:06 +0000 Subject: [PATCH 12/12] deploy: redeploy SIP, TokenZap --- .../arbitrum/SynapseIntentPreviewer.json | 16 +++++++++--- .../deployments/arbitrum/TokenZapV1.json | 26 ++++++++++++++++--- .../optimism/SynapseIntentPreviewer.json | 16 +++++++++--- .../deployments/optimism/TokenZapV1.json | 26 ++++++++++++++++--- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json b/packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json index fb6bdf01ec..2bca83a911 100644 --- a/packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json +++ b/packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json @@ -1,9 +1,9 @@ { - "address": "0xE184826D4aBC2798134abE7e2Fd72156827Fc7EA", + "address": "0x9519E8D136d0a89d7e10D1a66C97249E0135544B", "constructorArgs": "0x", "receipt": { - "hash": "0xdee35d80aa7aa61e76d152e22676067769f2923c85ed5e96d55c807ac9418277", - "blockNumber": 281941808 + "hash": "0xb34f3d918399ac6fa599ecedfdd4a47bd993f4f0e401698d6256dab2fd928ab9", + "blockNumber": 282619262 }, "abi": [ { @@ -28,6 +28,11 @@ "type": "address", "internalType": "address" }, + { + "name": "forwardTo", + "type": "address", + "internalType": "address" + }, { "name": "tokenIn", "type": "address", @@ -80,6 +85,11 @@ ], "stateMutability": "view" }, + { + "type": "error", + "name": "SIP__NoOpForwardNotSupported", + "inputs": [] + }, { "type": "error", "name": "SIP__PoolTokenMismatch", diff --git a/packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json b/packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json index 6a05ab4935..016ce5a627 100644 --- a/packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json +++ b/packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json @@ -1,9 +1,9 @@ { - "address": "0xcae5baD754dC99cDA1d58415C5eeeb92e1279F69", + "address": "0x6327797F149a75D506aFda46D5fCE6E74fC409D5", "constructorArgs": "0x", "receipt": { - "hash": "0x2a3ca21354283f25dd3e27358b642bc9d33e3a780afb99c98729b7697e903ff6", - "blockNumber": 281258026 + "hash": "0x961a29a85c10275a0d1921ef606f3ed45a79e9106e379b5efd7ae14faa30b1fe", + "blockNumber": 282619267 }, "abi": [ { @@ -70,6 +70,16 @@ "name": "amountPosition", "type": "uint256", "internalType": "uint256" + }, + { + "name": "finalToken", + "type": "address", + "internalType": "address" + }, + { + "name": "forwardTo", + "type": "address", + "internalType": "address" } ], "outputs": [ @@ -148,6 +158,11 @@ } ] }, + { + "type": "error", + "name": "TokenZapV1__FinalTokenBalanceZero", + "inputs": [] + }, { "type": "error", "name": "TokenZapV1__PayloadLengthAboveMax", @@ -158,6 +173,11 @@ "name": "TokenZapV1__TargetZeroAddress", "inputs": [] }, + { + "type": "error", + "name": "TokenZapV1__TokenZeroAddress", + "inputs": [] + }, { "type": "error", "name": "ZapDataV1__InvalidEncoding", diff --git a/packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json b/packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json index e53f8622ad..7a3f5d4950 100644 --- a/packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json +++ b/packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json @@ -1,9 +1,9 @@ { - "address": "0xE184826D4aBC2798134abE7e2Fd72156827Fc7EA", + "address": "0x9519E8D136d0a89d7e10D1a66C97249E0135544B", "constructorArgs": "0x", "receipt": { - "hash": "0x57d5d445810008804844d57d54ab99ab5229e87a206996f9cafc67494bf7fd77", - "blockNumber": 128944965 + "hash": "0x928a7db8741fb992934302f73e076f7630075151384529b538cb133e797c4bac", + "blockNumber": 129029951 }, "abi": [ { @@ -28,6 +28,11 @@ "type": "address", "internalType": "address" }, + { + "name": "forwardTo", + "type": "address", + "internalType": "address" + }, { "name": "tokenIn", "type": "address", @@ -80,6 +85,11 @@ ], "stateMutability": "view" }, + { + "type": "error", + "name": "SIP__NoOpForwardNotSupported", + "inputs": [] + }, { "type": "error", "name": "SIP__PoolTokenMismatch", diff --git a/packages/contracts-rfq/deployments/optimism/TokenZapV1.json b/packages/contracts-rfq/deployments/optimism/TokenZapV1.json index ed4d8ef25b..7663f76e78 100644 --- a/packages/contracts-rfq/deployments/optimism/TokenZapV1.json +++ b/packages/contracts-rfq/deployments/optimism/TokenZapV1.json @@ -1,9 +1,9 @@ { - "address": "0xcae5baD754dC99cDA1d58415C5eeeb92e1279F69", + "address": "0x6327797F149a75D506aFda46D5fCE6E74fC409D5", "constructorArgs": "0x", "receipt": { - "hash": "0x9bba14716cfff19933c2d0c1e83fc1400df60909c49e27d633f03ee1f4740e92", - "blockNumber": 128859363 + "hash": "0xc306e272b5daa98006c1d9009246fac697c258ed8fb6012ab19f5ef5376899b9", + "blockNumber": 129029951 }, "abi": [ { @@ -70,6 +70,16 @@ "name": "amountPosition", "type": "uint256", "internalType": "uint256" + }, + { + "name": "finalToken", + "type": "address", + "internalType": "address" + }, + { + "name": "forwardTo", + "type": "address", + "internalType": "address" } ], "outputs": [ @@ -148,6 +158,11 @@ } ] }, + { + "type": "error", + "name": "TokenZapV1__FinalTokenBalanceZero", + "inputs": [] + }, { "type": "error", "name": "TokenZapV1__PayloadLengthAboveMax", @@ -158,6 +173,11 @@ "name": "TokenZapV1__TargetZeroAddress", "inputs": [] }, + { + "type": "error", + "name": "TokenZapV1__TokenZeroAddress", + "inputs": [] + }, { "type": "error", "name": "ZapDataV1__InvalidEncoding",