From 6fd65c43f0946169b668941b71ad7fd702865899 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:13:50 +0000 Subject: [PATCH 01/15] feat: scaffold IntentRouter --- .../interfaces/ISynapseIntentRouter.sol | 49 +++++++++++++++++++ .../interfaces/ISynapseIntentRouterErrors.sol | 9 ++++ .../contracts/router/SynapseIntentRouter.sol | 23 +++++++++ 3 files changed, 81 insertions(+) create mode 100644 packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol create mode 100644 packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol create mode 100644 packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol diff --git a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol new file mode 100644 index 0000000000..4efa1efcee --- /dev/null +++ b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface ISynapseIntentRouter { + /// @notice Parameters for a single Zap step. + /// @param token Address of the token to use for the step + /// @param amount Amount of tokens to use for the step (type(uint256).max to use the full ZapRecipient balance) + /// @param msgValue Amount of native token to supply for the step, out of the total `msg.value` used for the + /// `fulfillIntent` call (could differ from `amount` regardless of the token type) + /// @param zapData Instructions for the ZapRecipient contract on how to execute the Zap + struct StepParams { + address token; + uint256 amount; + uint256 msgValue; + bytes zapData; + } + + /// @notice Kindly ask SIR to complete the provided intent by completing a series of Zap steps using the + /// provided ZapRecipient contract. + /// - Each step is verified to be a correct Zap as per `IZapRecipient` specification. + /// - The amounts used for each step can be predetermined or based on the proceeds from the previous steps. + /// - SIR does not perform any checks on the Zap Data; the user is responsible for ensuring correct encoding. + /// - The user is responsible for selecting the correct ZapRecipient for their intent: ZapRecipient must be + /// able to modify the Zap Data to adjust to possible changes in the passed amount value. + /// @dev Typical workflow involves a series of preparation steps followed by the last step representing the user + /// intent such as bridging, depositing, or a simple transfer to the final recipient. The ZapRecipient must be + /// the funds recipient for the preparation steps, while the final recipient must be used for the last step. + /// @dev This function will revert in any of the following cases: + /// - The deadline has passed. + /// - The array of StepParams is empty. + /// - The amount of tokens to use for the last step is below the specified minimum. + /// - Any step fails. + /// - `msg.value` does not match `sum(steps[i].msgValue)`. + /// @param zapRecipient Address of the IZapRecipient contract to use for the Zap steps + /// @param amountIn Initial amount of tokens (steps[0].token) to transfer into ZapRecipient + /// @param minLastStepAmountIn Minimum amount of tokens (steps[N-1].token) to use for the last step + /// @param deadline Deadline for the intent to be completed + /// @param steps Parameters for each step. Use amount = type(uint256).max for steps that + /// should use the full ZapRecipient balance. + function completeIntent( + address zapRecipient, + uint256 amountIn, + uint256 minLastStepAmountIn, + uint256 deadline, + StepParams[] memory steps + ) + external + payable; +} diff --git a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol new file mode 100644 index 0000000000..4305a9a471 --- /dev/null +++ b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface ISynapseIntentRouterErrors { + error SIR__AmountInsufficient(); + error SIR__DeadlineExceeded(); + error SIR__MsgValueIncorrect(); + error SIR__StepsNotProvided(); +} diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol new file mode 100644 index 0000000000..89f7a51f2d --- /dev/null +++ b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ + +import {ISynapseIntentRouter} from "../interfaces/ISynapseIntentRouter.sol"; +import {ISynapseIntentRouterErrors} from "../interfaces/ISynapseIntentRouterErrors.sol"; + +contract SynapseIntentRouter is ISynapseIntentRouter, ISynapseIntentRouterErrors { + /// @inheritdoc ISynapseIntentRouter + function completeIntent( + address zapRecipient, + uint256 amountIn, + uint256 minLastStepAmountIn, + uint256 deadline, + StepParams[] calldata steps + ) + external + payable + { + // TODO: implement + } +} From 12e6e8bf3052bd03993819db65e327c759b361cd Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:14:52 +0000 Subject: [PATCH 02/15] feat: initial implementation --- .../interfaces/ISynapseIntentRouterErrors.sol | 3 + .../contracts/router/SynapseIntentRouter.sol | 87 ++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol index 4305a9a471..45c4902537 100644 --- a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol +++ b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol @@ -6,4 +6,7 @@ interface ISynapseIntentRouterErrors { error SIR__DeadlineExceeded(); error SIR__MsgValueIncorrect(); error SIR__StepsNotProvided(); + error SIR__TokenNotContract(); + error SIR__ZapIncorrectReturnValue(); + error SIR__ZapNoReturnValue(); } diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol index 89f7a51f2d..0b6297d280 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol @@ -5,8 +5,22 @@ pragma solidity 0.8.24; import {ISynapseIntentRouter} from "../interfaces/ISynapseIntentRouter.sol"; import {ISynapseIntentRouterErrors} from "../interfaces/ISynapseIntentRouterErrors.sol"; +import {IZapRecipient} from "../interfaces/IZapRecipient.sol"; + +// ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; contract SynapseIntentRouter is ISynapseIntentRouter, ISynapseIntentRouterErrors { + using SafeERC20 for IERC20; + + /// @notice The address reserved for the native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc.). + address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /// @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; + /// @inheritdoc ISynapseIntentRouter function completeIntent( address zapRecipient, @@ -18,6 +32,77 @@ contract SynapseIntentRouter is ISynapseIntentRouter, ISynapseIntentRouterErrors external payable { - // TODO: implement + // Validate the input parameters before proceeding. + uint256 length = steps.length; + if (block.timestamp > deadline) revert SIR__DeadlineExceeded(); + if (length == 0) revert SIR__StepsNotProvided(); + + // Transfer the input asset from the user to ZapRecipient. `steps[0]` exists as per check above. + _transferInputAsset(zapRecipient, steps[0].token, amountIn); + + // Perform the Zap steps, using predetermined amounts or the full balance of ZapRecipient, if instructed. + uint256 totalUsedMsgValue = 0; + for (uint256 i = 0; i < length; i++) { + address token = steps[i].token; + uint256 msgValue = steps[i].msgValue; + + // Adjust amount to be the full balance, if needed. + amountIn = steps[i].amount; + if (amountIn == FULL_BALANCE) { + amountIn = token == NATIVE_GAS_TOKEN + // Existing native balance + msg.value that will be forwarded + ? zapRecipient.balance + msgValue + : IERC20(token).balanceOf(zapRecipient); + } + + _performZap({ + zapRecipient: zapRecipient, + msgValue: msgValue, + zapRecipientCallData: abi.encodeCall(IZapRecipient.zap, (token, amountIn, steps[i].zapData)) + }); + unchecked { + // Can do unchecked addition here since we're guaranteed that the sum of all msg.value + // used for the Zaps won't overflow. + totalUsedMsgValue += msgValue; + } + } + + // Verify amountIn used for the last step, and that we fully spent `msg.value`. + if (amountIn < minLastStepAmountIn) revert SIR__AmountInsufficient(); + if (totalUsedMsgValue < msg.value) revert SIR__MsgValueIncorrect(); + } + + // ═════════════════════════════════════════════ INTERNAL METHODS ══════════════════════════════════════════════════ + + /// @notice Transfers the input asset from the user into ZapRecipient custody. This asset will later be + /// used to perform the zap steps. + function _transferInputAsset(address zapRecipient, address token, uint256 amount) internal { + if (token == NATIVE_GAS_TOKEN) { + // For the native gas token, we just need to check that the supplied `msg.value` is correct. + // We will later forward `msg.value` in the series of the steps using `StepParams.msgValue`. + if (amount != msg.value) revert SIR__MsgValueIncorrect(); + } else { + // For ERC20s, token is transferred from the user to ZapRecipient before performing the zap steps. + // Throw an explicit error if the provided token address is not a contract. + if (token.code.length == 0) revert SIR__TokenNotContract(); + IERC20(token).safeTransferFrom(msg.sender, zapRecipient, amount); + } + } + + /// @notice Performs a Zap step, using the provided msg.value and calldata. + /// Validates the return data from ZapRecipient as per `IZapRecipient` specification. + function _performZap(address zapRecipient, uint256 msgValue, bytes memory zapRecipientCallData) internal { + // Perform the low-level call to ZapRecipient, bubbling up any revert reason. + bytes memory returnData = + Address.functionCallWithValue({target: zapRecipient, data: zapRecipientCallData, value: msgValue}); + + // Explicit revert if no return data at all. + if (returnData.length == 0) revert SIR__ZapNoReturnValue(); + // Check that exactly a single return value was returned. + if (returnData.length != 32) revert SIR__ZapIncorrectReturnValue(); + // Return value should be abi-encoded hook function selector. + if (bytes32(returnData) != bytes32(IZapRecipient.zap.selector)) { + revert SIR__ZapIncorrectReturnValue(); + } } } From aac2b06de0bf2d8fed841ab10159fc6a697be193 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:29:26 +0000 Subject: [PATCH 03/15] test: single step --- .../contracts-rfq/test/mocks/PoolMock.sol | 44 ++ .../contracts-rfq/test/mocks/WETHMock.sol | 10 +- .../test/router/SynapseIntentRouter.t.sol | 384 ++++++++++++++++++ 3 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 packages/contracts-rfq/test/mocks/PoolMock.sol create mode 100644 packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol diff --git a/packages/contracts-rfq/test/mocks/PoolMock.sol b/packages/contracts-rfq/test/mocks/PoolMock.sol new file mode 100644 index 0000000000..7ec46f9479 --- /dev/null +++ b/packages/contracts-rfq/test/mocks/PoolMock.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// solhint-disable no-empty-blocks +/// @notice Pool mock for testing purposes. DO NOT USE IN PRODUCTION. +contract PoolMock { + using SafeERC20 for IERC20; + + address public immutable token0; + address public immutable token1; + + uint256 public ratioWei = 1e18; + + error PoolMock__TokenNotSupported(); + + constructor(address token0_, address token1_) { + token0 = token0_; + token1 = token1_; + } + + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testPoolMock() external {} + + function setRatioWei(uint256 ratioWei_) external { + ratioWei = ratioWei_; + } + + function swap(uint256 amountIn, address tokenIn) external returns (uint256 amountOut) { + address tokenOut; + if (tokenIn == token0) { + tokenOut = token1; + amountOut = amountIn * ratioWei / 1e18; + } else if (tokenIn == token1) { + tokenOut = token0; + amountOut = amountIn * 1e18 / ratioWei; + } else { + revert PoolMock__TokenNotSupported(); + } + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(tokenOut).safeTransfer(msg.sender, amountOut); + } +} diff --git a/packages/contracts-rfq/test/mocks/WETHMock.sol b/packages/contracts-rfq/test/mocks/WETHMock.sol index 75b5c4d5dd..ae79a21e1a 100644 --- a/packages/contracts-rfq/test/mocks/WETHMock.sol +++ b/packages/contracts-rfq/test/mocks/WETHMock.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {CommonBase} from "forge-std/Base.sol"; + // solhint-disable no-empty-blocks /// @notice WETH mock for testing purposes. DO NOT USE IN PRODUCTION. -contract WETHMock is ERC20 { +contract WETHMock is ERC20, CommonBase { constructor() ERC20("Mock Wrapped Ether", "Mock WETH") {} receive() external payable { @@ -16,6 +18,12 @@ contract WETHMock is ERC20 { /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. function testWETHMock() external {} + function mint(address to, uint256 amount) external { + uint256 newBalance = address(this).balance + amount; + vm.deal(address(this), newBalance); + _mint(to, amount); + } + function withdraw(uint256 amount) external { _burn(msg.sender, amount); Address.sendValue(payable(msg.sender), amount); diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol new file mode 100644 index 0000000000..2d0b69757f --- /dev/null +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { + ISynapseIntentRouter, + ISynapseIntentRouterErrors, + SynapseIntentRouter +} from "../../contracts/router/SynapseIntentRouter.sol"; +import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; + +import {MockERC20} from "../MockERC20.sol"; +import {PoolMock} from "../mocks/PoolMock.sol"; +import {SimpleVaultMock} from "../mocks/SimpleVaultMock.sol"; +import {WETHMock} from "../mocks/WETHMock.sol"; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { + address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint256 internal constant AMOUNT = 1 ether; + uint256 internal constant FULL_BALANCE = type(uint256).max; + + SynapseIntentRouter internal router; + TokenZapV1 internal tokenZap; + + MockERC20 internal erc20; + WETHMock internal weth; + PoolMock internal pool; + SimpleVaultMock internal vault; + + address internal user; + + function setUp() public { + router = new SynapseIntentRouter(); + tokenZap = new TokenZapV1(); + + erc20 = new MockERC20("TKN", 18); + weth = new WETHMock(); + pool = new PoolMock(address(erc20), address(weth)); + vault = new SimpleVaultMock(); + + user = makeAddr("User"); + + // Deal funds to the user + erc20.mint(user, 10 * AMOUNT); + weth.mint(user, 10 * AMOUNT); + deal(user, 10 * AMOUNT); + + // Deal funds to the pool + erc20.mint(address(pool), 1000 * AMOUNT); + weth.mint(address(pool), 1000 * AMOUNT); + deal(address(pool), 1000 * AMOUNT); + + // Approve the router + vm.prank(user); + erc20.approve(address(router), type(uint256).max); + vm.prank(user); + weth.approve(address(router), type(uint256).max); + } + + function getDepositZapData(address token) public view returns (bytes memory) { + return tokenZap.encodeZapData({ + target: address(vault), + // Use placeholder zero amount + payload: abi.encodeCall(vault.deposit, (token, 0, user)), + // Amount is encoded as the second parameter + amountPosition: 4 + 32 + }); + } + + function completeUserIntent( + uint256 msgValue, + uint256 amountIn, + uint256 minLastStepAmountIn, + uint256 deadline, + ISynapseIntentRouter.StepParams[] memory steps + ) + public + virtual + { + vm.prank(user); + router.completeIntent{value: msgValue}({ + zapRecipient: address(tokenZap), + amountIn: amountIn, + minLastStepAmountIn: minLastStepAmountIn, + deadline: deadline, + steps: steps + }); + } + + function checkRevertMsgValueAboveExpectedWithERC20( + ISynapseIntentRouter.StepParams[] memory steps, + uint256 lastStepAmountIn + ) + public + { + vm.expectRevert(SIR__MsgValueIncorrect.selector); + completeUserIntent({ + msgValue: 1, + amountIn: AMOUNT, + minLastStepAmountIn: lastStepAmountIn, + deadline: block.timestamp, + steps: steps + }); + } + + function checkRevertsMsgValueAboveExpectedWithNative( + ISynapseIntentRouter.StepParams[] memory steps, + uint256 lastStepAmountIn + ) + public + { + // Just msg.value is too high + vm.expectRevert(SIR__MsgValueIncorrect.selector); + completeUserIntent({ + msgValue: AMOUNT + 1, + amountIn: AMOUNT, + minLastStepAmountIn: lastStepAmountIn, + deadline: block.timestamp, + steps: steps + }); + // Both msg.value and amountIn are too high + vm.expectRevert(SIR__MsgValueIncorrect.selector); + completeUserIntent({ + msgValue: AMOUNT + 1, + amountIn: AMOUNT + 1, + minLastStepAmountIn: lastStepAmountIn, + deadline: block.timestamp, + steps: steps + }); + } + + function checkRevertsMsgValueBelowExpectedWithNative( + ISynapseIntentRouter.StepParams[] memory steps, + uint256 lastStepAmountIn + ) + public + { + // Just msg.value is too low + vm.expectRevert(SIR__MsgValueIncorrect.selector); + completeUserIntent({ + msgValue: AMOUNT - 1, + amountIn: AMOUNT, + minLastStepAmountIn: lastStepAmountIn, + deadline: block.timestamp, + steps: steps + }); + // Both msg.value and amountIn are too low + vm.expectRevert(abi.encodeWithSelector(Address.AddressInsufficientBalance.selector, router)); + completeUserIntent({ + msgValue: AMOUNT - 1, + amountIn: AMOUNT - 1, + minLastStepAmountIn: lastStepAmountIn, + deadline: block.timestamp, + steps: steps + }); + } + + function checkRevertDeadlineExceeded( + uint256 msgValue, + uint256 lastStepAmountIn, + ISynapseIntentRouter.StepParams[] memory steps + ) + public + { + vm.expectRevert(SIR__DeadlineExceeded.selector); + completeUserIntent({ + msgValue: msgValue, + amountIn: AMOUNT, + minLastStepAmountIn: lastStepAmountIn, + deadline: block.timestamp - 1, + steps: steps + }); + } + + function checkRevertAmountInsufficient( + uint256 msgValue, + uint256 lastStepAmountIn, + ISynapseIntentRouter.StepParams[] memory steps + ) + public + { + vm.expectRevert(SIR__AmountInsufficient.selector); + completeUserIntent({ + msgValue: msgValue, + amountIn: AMOUNT, + minLastStepAmountIn: lastStepAmountIn + 1, + deadline: block.timestamp, + steps: steps + }); + } + + // ═══════════════════════════════════════════════ DEPOSIT ERC20 ═══════════════════════════════════════════════════ + + function getDepositERC20Steps(uint256 amount) public view returns (ISynapseIntentRouter.StepParams[] memory) { + return toArray( + ISynapseIntentRouter.StepParams({ + token: address(erc20), + amount: amount, + msgValue: 0, + zapData: getDepositZapData(address(erc20)) + }) + ); + } + + function test_depositERC20_exactAmount() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(AMOUNT); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); + } + + function test_depositERC20_exactAmount_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(AMOUNT); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_depositERC20_exactAmount_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(AMOUNT); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_depositERC20_exactAmount_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(AMOUNT); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_depositERC20_fullBalance() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); + } + + function test_depositERC20_fullBalance_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_depositERC20_fullBalance_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_depositERC20_fullBalance_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + + // ══════════════════════════════════════════════ DEPOSIT NATIVE ═══════════════════════════════════════════════════ + + function getDepositNativeSteps(uint256 amount) public view returns (ISynapseIntentRouter.StepParams[] memory) { + return toArray( + ISynapseIntentRouter.StepParams({ + token: NATIVE_GAS_TOKEN, + amount: amount, + msgValue: AMOUNT, + zapData: getDepositZapData(NATIVE_GAS_TOKEN) + }) + ); + } + + function test_depositNative_exactAmount() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(AMOUNT); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); + } + + function test_depositNative_exactAmount_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(AMOUNT); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_depositNative_exactAmount_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(AMOUNT); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_depositNative_exactAmount_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(AMOUNT); + checkRevertsMsgValueAboveExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_depositNative_exactAmount_revert_msgValueBelowExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(AMOUNT); + checkRevertsMsgValueBelowExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT - 1}); + } + + function test_depositNative_fullBalance() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(FULL_BALANCE); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); + } + + function test_depositNative_fullBalance_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_depositNative_fullBalance_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_depositNative_fullBalance_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(FULL_BALANCE); + checkRevertsMsgValueAboveExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_depositNative_fullBalance_revert_msgValueBelowExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(FULL_BALANCE); + checkRevertsMsgValueBelowExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT - 1}); + } + + // ═══════════════════════════════════════════════════ UTILS ═══════════════════════════════════════════════════════ + + function toArray(ISynapseIntentRouter.StepParams memory a) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory arr) + { + arr = new ISynapseIntentRouter.StepParams[](1); + arr[0] = a; + return arr; + } + + function toArray( + ISynapseIntentRouter.StepParams memory a, + ISynapseIntentRouter.StepParams memory b + ) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory arr) + { + arr = new ISynapseIntentRouter.StepParams[](2); + arr[0] = a; + arr[1] = b; + return arr; + } + + function toArray( + ISynapseIntentRouter.StepParams memory a, + ISynapseIntentRouter.StepParams memory b, + ISynapseIntentRouter.StepParams memory c + ) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory arr) + { + arr = new ISynapseIntentRouter.StepParams[](3); + arr[0] = a; + arr[1] = b; + arr[2] = c; + return arr; + } +} From f3ecf9b59fb683dde1448394dbc836650832b99b Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:40:18 +0000 Subject: [PATCH 04/15] feat: scaffold checked/unchecked `completeIntent` --- .../interfaces/ISynapseIntentRouter.sol | 17 ++++++ .../interfaces/ISynapseIntentRouterErrors.sol | 1 + .../contracts/router/SynapseIntentRouter.sol | 17 +++++- .../SynapseIntentRouter.BalanceChecks.t.sol | 55 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol diff --git a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol index 4efa1efcee..592cfdc27d 100644 --- a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol +++ b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouter.sol @@ -22,6 +22,7 @@ interface ISynapseIntentRouter { /// - SIR does not perform any checks on the Zap Data; the user is responsible for ensuring correct encoding. /// - The user is responsible for selecting the correct ZapRecipient for their intent: ZapRecipient must be /// able to modify the Zap Data to adjust to possible changes in the passed amount value. + /// - SIR checks that the ZapRecipient balance for every token in `steps` has not increased after the last step. /// @dev Typical workflow involves a series of preparation steps followed by the last step representing the user /// intent such as bridging, depositing, or a simple transfer to the final recipient. The ZapRecipient must be /// the funds recipient for the preparation steps, while the final recipient must be used for the last step. @@ -37,6 +38,22 @@ interface ISynapseIntentRouter { /// @param deadline Deadline for the intent to be completed /// @param steps Parameters for each step. Use amount = type(uint256).max for steps that /// should use the full ZapRecipient balance. + function completeIntentWithBalanceChecks( + address zapRecipient, + uint256 amountIn, + uint256 minLastStepAmountIn, + uint256 deadline, + StepParams[] memory steps + ) + external + payable; + + /// @notice Kindly ask SIR to complete the provided intent by completing a series of Zap steps using the + /// provided ZapRecipient contract. + /// @dev This function is identical to `completeIntentWithBalanceChecks` except that it does not verify that + /// the ZapRecipient balance for every token in `steps` has not increased after the last Zap. + /// Anyone using this function must validate that the funds are fully spent by ZapRecipient + /// using other means like separate on-chain checks or off-chain simulation. function completeIntent( address zapRecipient, uint256 amountIn, diff --git a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol index 45c4902537..c0c82ae2a4 100644 --- a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol +++ b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentRouterErrors.sol @@ -7,6 +7,7 @@ interface ISynapseIntentRouterErrors { error SIR__MsgValueIncorrect(); error SIR__StepsNotProvided(); error SIR__TokenNotContract(); + error SIR__UnspentFunds(); error SIR__ZapIncorrectReturnValue(); error SIR__ZapNoReturnValue(); } diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol index 0b6297d280..62a9db8a2d 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol @@ -22,7 +22,7 @@ contract SynapseIntentRouter is ISynapseIntentRouter, ISynapseIntentRouterErrors uint256 internal constant FULL_BALANCE = type(uint256).max; /// @inheritdoc ISynapseIntentRouter - function completeIntent( + function completeIntentWithBalanceChecks( address zapRecipient, uint256 amountIn, uint256 minLastStepAmountIn, @@ -31,6 +31,21 @@ contract SynapseIntentRouter is ISynapseIntentRouter, ISynapseIntentRouterErrors ) external payable + { + // TODO: record and check balances + completeIntent(zapRecipient, amountIn, minLastStepAmountIn, deadline, steps); + } + + /// @inheritdoc ISynapseIntentRouter + function completeIntent( + address zapRecipient, + uint256 amountIn, + uint256 minLastStepAmountIn, + uint256 deadline, + StepParams[] calldata steps + ) + public + payable { // Validate the input parameters before proceeding. uint256 length = steps.length; diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol new file mode 100644 index 0000000000..fa2513432d --- /dev/null +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ISynapseIntentRouter, SynapseIntentRouterTest} from "./SynapseIntentRouter.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract SynapseIntentRouterBalanceChecksTest is SynapseIntentRouterTest { + function completeUserIntent( + uint256 msgValue, + uint256 amountIn, + uint256 minLastStepAmountIn, + uint256 deadline, + ISynapseIntentRouter.StepParams[] memory steps + ) + public + virtual + override + { + vm.prank(user); + router.completeIntentWithBalanceChecks{value: msgValue}({ + zapRecipient: address(tokenZap), + amountIn: amountIn, + minLastStepAmountIn: minLastStepAmountIn, + deadline: deadline, + steps: steps + }); + } + + // ═════════════════════════════════════════ SINGLE ZAP UNSPENT FUNDS ══════════════════════════════════════════════ + + function test_depositERC20_revert_unspentERC20() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20StepParams(AMOUNT); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT + 1, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + } + + function test_depositNative_revert_unspentNative() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeStepParams(AMOUNT); + steps[0].msgValue = AMOUNT + 1; + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: AMOUNT + 1, + amountIn: AMOUNT + 1, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + } +} From ab32c3a780863aae171525157c6c4470eb7a8374 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:50:02 +0000 Subject: [PATCH 05/15] feat: implement balance checks --- .../contracts/router/SynapseIntentRouter.sol | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol index 62a9db8a2d..33198add8f 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentRouter.sol @@ -32,8 +32,25 @@ contract SynapseIntentRouter is ISynapseIntentRouter, ISynapseIntentRouterErrors external payable { - // TODO: record and check balances + // Record the initial balances of ZapRecipient for each token. + uint256 length = steps.length; + uint256[] memory initialBalances = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + address token = steps[i].token; + initialBalances[i] = + token == NATIVE_GAS_TOKEN ? zapRecipient.balance : IERC20(token).balanceOf(zapRecipient); + } + + // Complete the intent as usual. completeIntent(zapRecipient, amountIn, minLastStepAmountIn, deadline, steps); + + // Verify that the ZapRecipient balance for each token has not increased. + for (uint256 i = 0; i < length; i++) { + address token = steps[i].token; + uint256 newBalance = + token == NATIVE_GAS_TOKEN ? zapRecipient.balance : IERC20(token).balanceOf(zapRecipient); + if (newBalance > initialBalances[i]) revert SIR__UnspentFunds(); + } } /// @inheritdoc ISynapseIntentRouter From 1d6d23eeaaa79f033e8ea458b8adafbc4859311a Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:57:49 +0000 Subject: [PATCH 06/15] test: add cases where TokenZap has non-zero initial amount --- .../SynapseIntentRouter.BalanceChecks.t.sol | 16 +++++-- .../test/router/SynapseIntentRouter.t.sol | 46 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol index fa2513432d..17993401ba 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol @@ -28,8 +28,8 @@ contract SynapseIntentRouterBalanceChecksTest is SynapseIntentRouterTest { // ═════════════════════════════════════════ SINGLE ZAP UNSPENT FUNDS ══════════════════════════════════════════════ - function test_depositERC20_revert_unspentERC20() public { - ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20StepParams(AMOUNT); + function test_depositERC20_exactAmount_revert_unspentERC20() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(AMOUNT); vm.expectRevert(SIR__UnspentFunds.selector); completeUserIntent({ msgValue: 0, @@ -40,8 +40,12 @@ contract SynapseIntentRouterBalanceChecksTest is SynapseIntentRouterTest { }); } - function test_depositNative_revert_unspentNative() public { - ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeStepParams(AMOUNT); + function test_depositERC20_exactAmount_extraFunds_revert_unspentERC20() public withExtraFunds { + test_depositERC20_exactAmount_revert_unspentERC20(); + } + + function test_depositNative_exactAmount_revert_unspentNative() public { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(AMOUNT); steps[0].msgValue = AMOUNT + 1; vm.expectRevert(SIR__UnspentFunds.selector); completeUserIntent({ @@ -52,4 +56,8 @@ contract SynapseIntentRouterBalanceChecksTest is SynapseIntentRouterTest { steps: steps }); } + + function test_depositNative_exactAmount_extraFunds_revert_unspentNative() public withExtraFunds { + test_depositNative_exactAmount_revert_unspentNative(); + } } diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol index 2d0b69757f..30d97e4c8c 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol @@ -20,6 +20,7 @@ import {Test} from "forge-std/Test.sol"; contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 internal constant AMOUNT = 1 ether; + uint256 internal constant EXTRA_FUNDS = 0.1337 ether; uint256 internal constant FULL_BALANCE = type(uint256).max; SynapseIntentRouter internal router; @@ -32,6 +33,13 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { address internal user; + modifier withExtraFunds() { + erc20.mint(address(tokenZap), EXTRA_FUNDS); + weth.mint(address(tokenZap), EXTRA_FUNDS); + deal(address(tokenZap), EXTRA_FUNDS); + _; + } + function setUp() public { router = new SynapseIntentRouter(); tokenZap = new TokenZapV1(); @@ -218,6 +226,11 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); } + /// @notice Extra funds should have no effect on "exact amount" instructions. + function test_depositERC20_exactAmount_extraFunds() public withExtraFunds { + test_depositERC20_exactAmount(); + } + function test_depositERC20_exactAmount_revert_deadlineExceeded() public { ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(AMOUNT); checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: AMOUNT, steps: steps}); @@ -246,6 +259,20 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); } + /// @notice Extra funds should be used with "full balance" instructions. + function test_depositERC20_fullBalance_extraFunds() public withExtraFunds { + ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, address(erc20)), AMOUNT + EXTRA_FUNDS); + } + function test_depositERC20_fullBalance_revert_deadlineExceeded() public { ISynapseIntentRouter.StepParams[] memory steps = getDepositERC20Steps(FULL_BALANCE); checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: AMOUNT, steps: steps}); @@ -287,6 +314,11 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); } + /// @notice Extra funds should have no effect on "exact amount" instructions. + function test_depositNative_exactAmount_extraFunds() public withExtraFunds { + test_depositNative_exactAmount(); + } + function test_depositNative_exactAmount_revert_deadlineExceeded() public { ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(AMOUNT); checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); @@ -320,6 +352,20 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); } + /// @notice Extra funds should be used with "full balance" instructions. + function test_depositNative_fullBalance_extraFunds() public withExtraFunds { + ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(FULL_BALANCE); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT + EXTRA_FUNDS); + } + function test_depositNative_fullBalance_revert_deadlineExceeded() public { ISynapseIntentRouter.StepParams[] memory steps = getDepositNativeSteps(FULL_BALANCE); checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); From 0a3a1ad8f0045999465212dceebb168a3d2fb5c9 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:38:57 +0000 Subject: [PATCH 07/15] test: double step cases (erc20 and/or native combos) --- .../SynapseIntentRouter.BalanceChecks.t.sol | 144 +++++ .../test/router/SynapseIntentRouter.t.sol | 607 +++++++++++++++++- 2 files changed, 750 insertions(+), 1 deletion(-) diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol index 17993401ba..af1e6f5056 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.BalanceChecks.t.sol @@ -60,4 +60,148 @@ contract SynapseIntentRouterBalanceChecksTest is SynapseIntentRouterTest { function test_depositNative_exactAmount_extraFunds_revert_unspentNative() public withExtraFunds { test_depositNative_exactAmount_revert_unspentNative(); } + + // ═════════════════════════════════════════ DOUBLE ZAP UNSPENT FUNDS ══════════════════════════════════════════════ + + function test_swapDepositERC20_exactAmounts_revert_unspentERC20() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, amountDeposit); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT + 1, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + } + + function test_swapDepositERC20_exactAmounts_revert_unspentWETH() public { + uint256 amountReduced = AMOUNT * TOKEN_PRICE - 1; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, amountReduced); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountReduced, + deadline: block.timestamp, + steps: steps + }); + } + + function test_swapDepositERC20_exactAmounts_extraFunds_revert_unspentERC20() public withExtraFunds { + test_swapDepositERC20_exactAmounts_revert_unspentERC20(); + } + + function test_swapDepositERC20_exactAmounts_extraFunds_revert_unspentWETH() public withExtraFunds { + test_swapDepositERC20_exactAmounts_revert_unspentWETH(); + } + + function test_swapDepositERC20_exactAmount1_extraFunds_revertWithBalanceChecks() public override withExtraFunds { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, amountDeposit); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + } + + function test_wrapDepositWETH_exactAmounts_revert_unspentNative() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, AMOUNT); + steps[0].msgValue = AMOUNT + 1; + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: AMOUNT + 1, + amountIn: AMOUNT + 1, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + } + + function test_wrapDepositWETH_exactAmounts_revert_unspentWETH() public { + uint256 amountReduced = AMOUNT - 1; + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, amountReduced); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: amountReduced, + deadline: block.timestamp, + steps: steps + }); + } + + function test_wrapDepositWETH_exactAmounts_extraFunds_revert_unspentNative() public withExtraFunds { + test_wrapDepositWETH_exactAmounts_revert_unspentNative(); + } + + function test_wrapDepositWETH_exactAmounts_extraFunds_revert_unspentWETH() public withExtraFunds { + test_wrapDepositWETH_exactAmounts_revert_unspentWETH(); + } + + function test_wrapDepositWETH_exactAmount1_extraFunds_revertWithBalanceChecks() public override withExtraFunds { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, AMOUNT); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + } + + function test_unwrapDepositNative_exactAmounts_revert_unspentWETH() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, AMOUNT); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT + 1, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + } + + function test_unwrapDepositNative_exactAmounts_revert_unspentNative() public { + uint256 amountReduced = AMOUNT - 1; + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, amountReduced); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountReduced, + deadline: block.timestamp, + steps: steps + }); + } + + function test_unwrapDepositNative_exactAmounts_extraFunds_revert_unspentWETH() public withExtraFunds { + test_unwrapDepositNative_exactAmounts_revert_unspentWETH(); + } + + function test_unwrapDepositNative_exactAmounts_extraFunds_revert_unspentNative() public withExtraFunds { + test_unwrapDepositNative_exactAmounts_revert_unspentNative(); + } + + function test_unwrapDepositNative_exactAmount1_extraFunds_revertWithBalanceChecks() + public + override + withExtraFunds + { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, AMOUNT); + vm.expectRevert(SIR__UnspentFunds.selector); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + 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 30d97e4c8c..9343889949 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol @@ -21,6 +21,7 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 internal constant AMOUNT = 1 ether; uint256 internal constant EXTRA_FUNDS = 0.1337 ether; + uint256 internal constant TOKEN_PRICE = 2; // in ETH uint256 internal constant FULL_BALANCE = type(uint256).max; SynapseIntentRouter internal router; @@ -46,9 +47,11 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { erc20 = new MockERC20("TKN", 18); weth = new WETHMock(); - pool = new PoolMock(address(erc20), address(weth)); vault = new SimpleVaultMock(); + pool = new PoolMock(address(weth), address(erc20)); + pool.setRatioWei(TOKEN_PRICE * 1e18); + user = makeAddr("User"); // Deal funds to the user @@ -68,6 +71,34 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { weth.approve(address(router), type(uint256).max); } + function getWrapZapData() public view returns (bytes memory) { + return tokenZap.encodeZapData({ + target: address(weth), + payload: abi.encodeCall(weth.deposit, ()), + // Amount is not encoded + amountPosition: type(uint256).max + }); + } + + function getUnwrapZapData() 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 + }); + } + + function getSwapZapData(address token) public view returns (bytes memory) { + 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 + }); + } + function getDepositZapData(address token) public view returns (bytes memory) { return tokenZap.encodeZapData({ target: address(vault), @@ -386,6 +417,580 @@ contract SynapseIntentRouterTest is Test, ISynapseIntentRouterErrors { checkRevertsMsgValueBelowExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT - 1}); } + // ═══════════════════════════════════════════ SWAP & DEPOSIT ERC20 ════════════════════════════════════════════════ + + function getSwapDepositERC20Steps( + uint256 amountSwap, + uint256 amountDeposit + ) + public + view + returns (ISynapseIntentRouter.StepParams[] memory) + { + return toArray( + // WETH -> ERC20 + ISynapseIntentRouter.StepParams({ + token: address(weth), + amount: amountSwap, + msgValue: 0, + zapData: getSwapZapData(address(weth)) + }), + // deposit ERC20 + ISynapseIntentRouter.StepParams({ + token: address(erc20), + amount: amountDeposit, + msgValue: 0, + zapData: getDepositZapData(address(erc20)) + }) + ); + } + + function test_swapDepositERC20_exactAmounts() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, amountDeposit); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), amountDeposit); + } + + /// @notice Extra funds should have no effect on "exact amount" instructions. + function test_swapDepositERC20_exactAmounts_extraFunds() public withExtraFunds { + test_swapDepositERC20_exactAmounts(); + } + + function test_swapDepositERC20_exactAmounts_revert_deadlineExceeded() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, amountDeposit); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountDeposit, steps: steps}); + } + + function test_swapDepositERC20_exactAmounts_revert_lastStepAmountInsufficient() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, amountDeposit); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountDeposit + 1, steps: steps}); + } + + function test_swapDepositERC20_exactAmounts_revert_msgValueAboveExpected() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, amountDeposit); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountDeposit}); + } + + function test_swapDepositERC20_exactAmount0() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), amountDeposit); + } + + /// @notice Extra funds should be used with the final "full balance" instruction. + function test_swapDepositERC20_exactAmount0_extraFunds() public withExtraFunds { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE + EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, address(erc20)), amountDeposit); + } + + function test_swapDepositERC20_exactAmount0_revert_deadlineExceeded() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountDeposit, steps: steps}); + } + + function test_swapDepositERC20_exactAmount0_revert_lastStepAmountInsufficient() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountDeposit + 1, steps: steps}); + } + + function test_swapDepositERC20_exactAmount0_revert_msgValueAboveExpected() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(AMOUNT, FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountDeposit}); + } + + function test_swapDepositERC20_exactAmount1() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, amountDeposit); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), amountDeposit); + } + + /// @notice Should succeed with extra funds if no balance checks are performed. + /// Last action is "use exact amount", so extra funds have no effect. + function test_swapDepositERC20_exactAmount1_extraFunds_revertWithBalanceChecks() public virtual withExtraFunds { + test_swapDepositERC20_exactAmount1(); + } + + function test_swapDepositERC20_exactAmount1_revert_deadlineExceeded() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, amountDeposit); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountDeposit, steps: steps}); + } + + function test_swapDepositERC20_exactAmount1_revert_lastStepAmountInsufficient() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, amountDeposit); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountDeposit + 1, steps: steps}); + } + + function test_swapDepositERC20_exactAmount1_revert_msgValueAboveExpected() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, amountDeposit); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountDeposit}); + } + + function test_swapDepositERC20_fullBalances() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), amountDeposit); + } + + /// @notice Extra funds should be used with both "full balance" instructions. + function test_swapDepositERC20_fullBalances_extraFunds() public withExtraFunds { + uint256 amountDeposit = (AMOUNT + EXTRA_FUNDS) * TOKEN_PRICE + EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, address(erc20)), amountDeposit); + } + + function test_swapDepositERC20_fullBalances_revert_deadlineExceeded() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: 0, lastStepAmountIn: amountDeposit, steps: steps}); + } + + function test_swapDepositERC20_fullBalances_revert_lastStepAmountInsufficient() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: 0, lastStepAmountIn: amountDeposit + 1, steps: steps}); + } + + function test_swapDepositERC20_fullBalances_revert_msgValueAboveExpected() public { + uint256 amountDeposit = AMOUNT * TOKEN_PRICE; + ISynapseIntentRouter.StepParams[] memory steps = getSwapDepositERC20Steps(FULL_BALANCE, FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: amountDeposit}); + } + + // ════════════════════════════════════════════ WRAP & DEPOSIT WETH ════════════════════════════════════════════════ + + function getWrapDepositWETHSteps( + uint256 amountWrap, + uint256 amountDeposit + ) + public + view + returns (ISynapseIntentRouter.StepParams[] memory) + { + return toArray( + // ETH -> WETH + ISynapseIntentRouter.StepParams({ + token: NATIVE_GAS_TOKEN, + amount: amountWrap, + msgValue: AMOUNT, + zapData: getWrapZapData() + }), + // deposit WETH + ISynapseIntentRouter.StepParams({ + token: address(weth), + amount: amountDeposit, + msgValue: 0, + zapData: getDepositZapData(address(weth)) + }) + ); + } + + function test_wrapDepositWETH_exactAmounts() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, AMOUNT); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); + } + + /// @notice Extra funds should have no effect on "exact amount" instructions. + function test_wrapDepositWETH_exactAmounts_extraFunds() public withExtraFunds { + test_wrapDepositWETH_exactAmounts(); + } + + function test_wrapDepositWETH_exactAmounts_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, AMOUNT); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_wrapDepositWETH_exactAmounts_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, AMOUNT); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_wrapDepositWETH_exactAmounts_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, AMOUNT); + checkRevertsMsgValueAboveExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_wrapDepositWETH_exactAmount0() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); + } + + /// @notice Extra funds should be used with the final "full balance" instruction. + function test_wrapDepositWETH_exactAmount0_extraFunds() public withExtraFunds { + uint256 amountDeposit = AMOUNT + EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, address(weth)), amountDeposit); + } + + function test_wrapDepositWETH_exactAmount0_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_wrapDepositWETH_exactAmount0_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_wrapDepositWETH_exactAmount0_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, FULL_BALANCE); + checkRevertsMsgValueAboveExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_wrapDepositWETH_exactAmount0_revert_msgValueBelowExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(AMOUNT, FULL_BALANCE); + checkRevertsMsgValueBelowExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT - 1}); + } + + function test_wrapDepositWETH_exactAmount1() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, AMOUNT); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); + } + + /// @notice Should succeed with extra funds if no balance checks are performed. + /// Last action is "use exact amount", so extra funds have no effect. + function test_wrapDepositWETH_exactAmount1_extraFunds_revertWithBalanceChecks() public virtual withExtraFunds { + test_wrapDepositWETH_exactAmount1(); + } + + function test_wrapDepositWETH_exactAmount1_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, AMOUNT); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_wrapDepositWETH_exactAmount1_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, AMOUNT); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_wrapDepositWETH_exactAmount1_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, AMOUNT); + checkRevertsMsgValueAboveExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_wrapDepositWETH_exactAmount1_revert_msgValueBelowExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, AMOUNT); + checkRevertsMsgValueBelowExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT - 1}); + } + + function test_wrapDepositWETH_fullBalances() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); + } + + /// @notice Extra funds should be used with both "full balance" instructions. + function test_wrapDepositWETH_fullBalances_extraFunds() public withExtraFunds { + uint256 amountDeposit = AMOUNT + 2 * EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: AMOUNT, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, address(weth)), amountDeposit); + } + + function test_wrapDepositWETH_fullBalances_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_wrapDepositWETH_fullBalances_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_wrapDepositWETH_fullBalances_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertsMsgValueAboveExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_wrapDepositWETH_fullBalances_revert_msgValueBelowExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getWrapDepositWETHSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertsMsgValueBelowExpectedWithNative({steps: steps, lastStepAmountIn: AMOUNT - 1}); + } + + // ══════════════════════════════════════════ UNWRAP & DEPOSIT NATIVE ══════════════════════════════════════════════ + + function getUnwrapDepositNativeSteps( + uint256 amountUnwrap, + uint256 amountDeposit + ) + public + view + returns (ISynapseIntentRouter.StepParams[] memory) + { + return toArray( + // WETH -> ETH + ISynapseIntentRouter.StepParams({ + token: address(weth), + amount: amountUnwrap, + msgValue: 0, + zapData: getUnwrapZapData() + }), + // Deposit ETH + ISynapseIntentRouter.StepParams({ + token: NATIVE_GAS_TOKEN, + amount: amountDeposit, + msgValue: 0, + zapData: getDepositZapData(NATIVE_GAS_TOKEN) + }) + ); + } + + function test_unwrapDepositNative_exactAmounts() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, AMOUNT); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); + } + + /// @notice Extra funds should have no effect on "exact amount" instructions. + function test_unwrapDepositNative_exactAmounts_extraFunds() public withExtraFunds { + test_unwrapDepositNative_exactAmounts(); + } + + function test_unwrapDepositNative_exactAmounts_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, AMOUNT); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_unwrapDepositNative_exactAmounts_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, AMOUNT); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_unwrapDepositNative_exactAmounts_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, AMOUNT); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_unwrapDepositNative_exactAmount0() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); + } + + /// @notice Extra funds should be used with the final "full balance" instruction. + function test_unwrapDepositNative_exactAmount0_extraFunds() public withExtraFunds { + uint256 amountDeposit = AMOUNT + EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), amountDeposit); + } + + function test_unwrapDepositNative_exactAmount0_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_unwrapDepositNative_exactAmount0_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_unwrapDepositNative_exactAmount0_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(AMOUNT, FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_unwrapDepositNative_exactAmount1() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, AMOUNT); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); + } + + /// @notice Should succeed with extra funds if no balance checks are performed. + /// Last action is "use exact amount", so extra funds have no effect. + function test_unwrapDepositNative_exactAmount1_extraFunds_revertWithBalanceChecks() public virtual withExtraFunds { + test_unwrapDepositNative_exactAmount1(); + } + + function test_unwrapDepositNative_exactAmount1_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, AMOUNT); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_unwrapDepositNative_exactAmount1_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, AMOUNT); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_unwrapDepositNative_exactAmount1_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, AMOUNT); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + + function test_unwrapDepositNative_fullBalances() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: AMOUNT, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), AMOUNT); + } + + /// @notice Extra funds should be used with both "full balance" instructions. + function test_unwrapDepositNative_fullBalances_extraFunds() public withExtraFunds { + uint256 amountDeposit = AMOUNT + 2 * EXTRA_FUNDS; + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, FULL_BALANCE); + completeUserIntent({ + msgValue: 0, + amountIn: AMOUNT, + minLastStepAmountIn: amountDeposit, + deadline: block.timestamp, + steps: steps + }); + // Check that the vault registered the deposit with the extra funds + assertEq(vault.balanceOf(user, NATIVE_GAS_TOKEN), amountDeposit); + } + + function test_unwrapDepositNative_fullBalances_revert_deadlineExceeded() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertDeadlineExceeded({msgValue: AMOUNT, lastStepAmountIn: AMOUNT, steps: steps}); + } + + function test_unwrapDepositNative_fullBalances_revert_lastStepAmountInsufficient() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertAmountInsufficient({msgValue: AMOUNT, lastStepAmountIn: AMOUNT + 1, steps: steps}); + } + + function test_unwrapDepositNative_fullBalances_revert_msgValueAboveExpected() public { + ISynapseIntentRouter.StepParams[] memory steps = getUnwrapDepositNativeSteps(FULL_BALANCE, FULL_BALANCE); + checkRevertMsgValueAboveExpectedWithERC20({steps: steps, lastStepAmountIn: AMOUNT}); + } + // ═══════════════════════════════════════════════════ UTILS ═══════════════════════════════════════════════════════ function toArray(ISynapseIntentRouter.StepParams memory a) From abcac427b2f1e6d6edb59280a7980b383635e610 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:02:37 +0000 Subject: [PATCH 08/15] feat: SIR, TokenZap deployment script --- packages/contracts-rfq/script/DeploySIR.s.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/contracts-rfq/script/DeploySIR.s.sol diff --git a/packages/contracts-rfq/script/DeploySIR.s.sol b/packages/contracts-rfq/script/DeploySIR.s.sol new file mode 100644 index 0000000000..0cd16705be --- /dev/null +++ b/packages/contracts-rfq/script/DeploySIR.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {SynapseScript} from "@synapsecns/solidity-devops/src/SynapseScript.sol"; + +// solhint-disable no-empty-blocks +contract DeploySIR is SynapseScript { + string public constant LATEST_SIR = "SynapseIntentRouter"; + string public constant LATEST_ZAP = "TokenZapV1"; + + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testDeploySIR() external {} + + function run() external broadcastWithHooks { + // TODO: create2 salts + deployAndSave({contractName: LATEST_SIR, constructorArgs: "", deployCodeFunc: cbDeployCreate2}); + deployAndSave({contractName: LATEST_ZAP, constructorArgs: "", deployCodeFunc: cbDeployCreate2}); + } +} From 3410d11d7a7391d041756d7fd1d8ad27471ac02f Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:06:17 +0000 Subject: [PATCH 09/15] deploy: test deploys for SIR --- .../arbitrum/SynapseIntentRouter.json | 211 ++++++++++++++++++ .../deployments/arbitrum/TokenZapV1.json | 183 +++++++++++++++ .../optimism/SynapseIntentRouter.json | 211 ++++++++++++++++++ .../deployments/optimism/TokenZapV1.json | 183 +++++++++++++++ 4 files changed, 788 insertions(+) create mode 100644 packages/contracts-rfq/deployments/arbitrum/SynapseIntentRouter.json create mode 100644 packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json create mode 100644 packages/contracts-rfq/deployments/optimism/SynapseIntentRouter.json create mode 100644 packages/contracts-rfq/deployments/optimism/TokenZapV1.json diff --git a/packages/contracts-rfq/deployments/arbitrum/SynapseIntentRouter.json b/packages/contracts-rfq/deployments/arbitrum/SynapseIntentRouter.json new file mode 100644 index 0000000000..6c10e5f25a --- /dev/null +++ b/packages/contracts-rfq/deployments/arbitrum/SynapseIntentRouter.json @@ -0,0 +1,211 @@ +{ + "address": "0x57203c65DeA2ded4EE4E303a9494bee04df030BF", + "constructorArgs": "0x", + "receipt": { + "hash": "0x5a6c34cc550a0b73a48f412018f04e97a868d60ee411364fa1a67427a0d2708b", + "blockNumber": 281258022 + }, + "abi": [ + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "completeIntent", + "inputs": [ + { + "name": "zapRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minLastStepAmountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "completeIntentWithBalanceChecks", + "inputs": [ + { + "name": "zapRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minLastStepAmountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AddressInsufficientBalance", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__AmountInsufficient", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__DeadlineExceeded", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__MsgValueIncorrect", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__StepsNotProvided", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__TokenNotContract", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__UnspentFunds", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__ZapIncorrectReturnValue", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__ZapNoReturnValue", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json b/packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json new file mode 100644 index 0000000000..6a05ab4935 --- /dev/null +++ b/packages/contracts-rfq/deployments/arbitrum/TokenZapV1.json @@ -0,0 +1,183 @@ +{ + "address": "0xcae5baD754dC99cDA1d58415C5eeeb92e1279F69", + "constructorArgs": "0x", + "receipt": { + "hash": "0x2a3ca21354283f25dd3e27358b642bc9d33e3a780afb99c98729b7697e903ff6", + "blockNumber": 281258026 + }, + "abi": [ + { + "type": "receive", + "stateMutability": "payable" + }, + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decodeZapData", + "inputs": [ + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "encodeZapData", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amountPosition", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "zap", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "payable" + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AddressInsufficientBalance", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenZapV1__PayloadLengthAboveMax", + "inputs": [] + }, + { + "type": "error", + "name": "TokenZapV1__TargetZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__InvalidEncoding", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__TargetZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__UnsupportedVersion", + "inputs": [ + { + "name": "version", + "type": "uint16", + "internalType": "uint16" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/contracts-rfq/deployments/optimism/SynapseIntentRouter.json b/packages/contracts-rfq/deployments/optimism/SynapseIntentRouter.json new file mode 100644 index 0000000000..03288efc1a --- /dev/null +++ b/packages/contracts-rfq/deployments/optimism/SynapseIntentRouter.json @@ -0,0 +1,211 @@ +{ + "address": "0x57203c65DeA2ded4EE4E303a9494bee04df030BF", + "constructorArgs": "0x", + "receipt": { + "hash": "0xf68cf0c65d39291cf7b293228ae1664ca8fb0b2afb32e6ed1ecbac80a38f4771", + "blockNumber": 128859363 + }, + "abi": [ + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "completeIntent", + "inputs": [ + { + "name": "zapRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minLastStepAmountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "completeIntentWithBalanceChecks", + "inputs": [ + { + "name": "zapRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minLastStepAmountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AddressInsufficientBalance", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__AmountInsufficient", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__DeadlineExceeded", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__MsgValueIncorrect", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__StepsNotProvided", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__TokenNotContract", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__UnspentFunds", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__ZapIncorrectReturnValue", + "inputs": [] + }, + { + "type": "error", + "name": "SIR__ZapNoReturnValue", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/contracts-rfq/deployments/optimism/TokenZapV1.json b/packages/contracts-rfq/deployments/optimism/TokenZapV1.json new file mode 100644 index 0000000000..ed4d8ef25b --- /dev/null +++ b/packages/contracts-rfq/deployments/optimism/TokenZapV1.json @@ -0,0 +1,183 @@ +{ + "address": "0xcae5baD754dC99cDA1d58415C5eeeb92e1279F69", + "constructorArgs": "0x", + "receipt": { + "hash": "0x9bba14716cfff19933c2d0c1e83fc1400df60909c49e27d633f03ee1f4740e92", + "blockNumber": 128859363 + }, + "abi": [ + { + "type": "receive", + "stateMutability": "payable" + }, + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decodeZapData", + "inputs": [ + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "encodeZapData", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "amountPosition", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "zap", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "payable" + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AddressInsufficientBalance", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "TokenZapV1__PayloadLengthAboveMax", + "inputs": [] + }, + { + "type": "error", + "name": "TokenZapV1__TargetZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__InvalidEncoding", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__TargetZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__UnsupportedVersion", + "inputs": [ + { + "name": "version", + "type": "uint16", + "internalType": "uint16" + } + ] + } + ] +} \ No newline at end of file From 394f78b9f83e2fb6e256367bd9a2bb465a132fb2 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:37:13 +0000 Subject: [PATCH 10/15] test: update to #3434 changes --- packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol index 9343889949..110be2f523 100644 --- a/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol +++ b/packages/contracts-rfq/test/router/SynapseIntentRouter.t.sol @@ -8,7 +8,7 @@ import { } from "../../contracts/router/SynapseIntentRouter.sol"; import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; -import {MockERC20} from "../MockERC20.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; import {PoolMock} from "../mocks/PoolMock.sol"; import {SimpleVaultMock} from "../mocks/SimpleVaultMock.sol"; import {WETHMock} from "../mocks/WETHMock.sol"; From 4843d571a9135d00717d676b62f1ece1ad09d19a Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:38:09 +0000 Subject: [PATCH 11/15] feat: scaffold SIP --- .../interfaces/ISynapseIntentPreviewer.sol | 26 +++++++++++++++++++ .../router/SynapseIntentPreviewer.sol | 21 +++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol create mode 100644 packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol diff --git a/packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol new file mode 100644 index 0000000000..9643a50f7d --- /dev/null +++ b/packages/contracts-rfq/contracts/interfaces/ISynapseIntentPreviewer.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ISynapseIntentRouter} from "./ISynapseIntentRouter.sol"; + +interface ISynapseIntentPreviewer { + /// @notice Preview the completion of a user intent. + /// @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 tokenIn Initial token for the intent + /// @param tokenOut Final token for the intent + /// @param amountIn Initial amount of tokens to use for the intent + /// @return amountOut Final amount of tokens to receive. Zero if the intent cannot be completed. + /// @return steps Steps to use in SynapseIntentRouter in order to complete the intent. + /// Empty if the intent cannot be completed, or if intent is a no-op (tokenIn == tokenOut). + function previewIntent( + address swapQuoter, + address tokenIn, + address tokenOut, + uint256 amountIn + ) + external + view + returns (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps); +} diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol new file mode 100644 index 0000000000..aeef811abe --- /dev/null +++ b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ + +import {ISynapseIntentRouter} from "../interfaces/ISynapseIntentRouter.sol"; + +contract SynapseIntentPreviewer { + function previewIntent( + address swapQuoter, + address tokenIn, + address tokenOut, + uint256 amountIn + ) + external + view + returns (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) + { + // TODO: implement + } +} From 4106cd524bf0a29b0bfe34a6964956c3f601803f Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:08:57 +0000 Subject: [PATCH 12/15] test: coverage for SIP --- .../interfaces/IDefaultExtendedPool.sol | 2 +- .../legacy/router/interfaces/IDefaultPool.sol | 2 +- .../test/harnesses/ZapDataV1Harness.sol | 3 + .../test/mocks/DefaultPoolMock.sol | 55 ++ .../test/mocks/SwapQuoterMock.sol | 20 + .../test/router/SynapseIntentPreviewer.t.sol | 472 ++++++++++++++++++ 6 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 packages/contracts-rfq/test/mocks/DefaultPoolMock.sol create mode 100644 packages/contracts-rfq/test/mocks/SwapQuoterMock.sol create mode 100644 packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol diff --git a/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultExtendedPool.sol b/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultExtendedPool.sol index 454ff8f33e..80e1d9ac1c 100644 --- a/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultExtendedPool.sol +++ b/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultExtendedPool.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; import {IDefaultPool} from "./IDefaultPool.sol"; diff --git a/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultPool.sol b/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultPool.sol index 195f71e221..c75bd50a65 100644 --- a/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultPool.sol +++ b/packages/contracts-rfq/contracts/legacy/router/interfaces/IDefaultPool.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; interface IDefaultPool { function swap( diff --git a/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol index b1b5cef18e..727e70fb22 100644 --- a/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol +++ b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.24; import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; contract ZapDataV1Harness { + uint16 public constant VERSION = ZapDataV1.VERSION; + uint16 public constant AMOUNT_NOT_PRESENT = ZapDataV1.AMOUNT_NOT_PRESENT; + function validateV1(bytes calldata encodedZapData) public pure { ZapDataV1.validateV1(encodedZapData); } diff --git a/packages/contracts-rfq/test/mocks/DefaultPoolMock.sol b/packages/contracts-rfq/test/mocks/DefaultPoolMock.sol new file mode 100644 index 0000000000..ef48c50010 --- /dev/null +++ b/packages/contracts-rfq/test/mocks/DefaultPoolMock.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IDefaultPool} from "../../contracts/legacy/router/interfaces/IDefaultPool.sol"; + +// solhint-disable no-empty-blocks +contract DefaultPoolMock is IDefaultPool { + uint8 private constant TOKENS = 3; + + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testDefaultPoolMock() external {} + + function swap( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 minDy, + uint256 deadline + ) + external + returns (uint256 amountOut) + {} + + function calculateSwap( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx + ) + external + view + returns (uint256 amountOut) + {} + + function swapStorage() + external + view + returns ( + uint256 initialA, + uint256 futureA, + uint256 initialATime, + uint256 futureATime, + uint256 swapFee, + uint256 adminFee, + address lpToken + ) + {} + + function getToken(uint8 index) external pure returns (address token) { + if (index < TOKENS) { + // Will be overridden by vm.mockCall + return address(uint160(1 + index)); + } + revert("Token does not exist"); + } +} diff --git a/packages/contracts-rfq/test/mocks/SwapQuoterMock.sol b/packages/contracts-rfq/test/mocks/SwapQuoterMock.sol new file mode 100644 index 0000000000..83f1b448f9 --- /dev/null +++ b/packages/contracts-rfq/test/mocks/SwapQuoterMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ISwapQuoter, LimitedToken, SwapQuery} from "../../contracts/legacy/rfq/interfaces/ISwapQuoter.sol"; + +// solhint-disable no-empty-blocks +contract SwapQuoterMock is ISwapQuoter { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testSwapQuoterMock() external {} + + function getAmountOut( + LimitedToken memory tokenIn, + address tokenOut, + uint256 amountIn + ) + external + view + returns (SwapQuery memory query) + {} +} diff --git a/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol b/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol new file mode 100644 index 0000000000..4d8c5b6ba3 --- /dev/null +++ b/packages/contracts-rfq/test/router/SynapseIntentPreviewer.t.sol @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ISynapseIntentRouter} from "../../contracts/interfaces/ISynapseIntentRouter.sol"; +import {IDefaultExtendedPool} from "../../contracts/legacy/router/interfaces/IDefaultExtendedPool.sol"; +import {Action, DefaultParams} from "../../contracts/legacy/router/libs/Structs.sol"; +import {SynapseIntentPreviewer} from "../../contracts/router/SynapseIntentPreviewer.sol"; + +import {ZapDataV1Harness} from "../harnesses/ZapDataV1Harness.sol"; + +import {DefaultPoolMock} from "../mocks/DefaultPoolMock.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; +import {LimitedToken, SwapQuery, SwapQuoterMock} from "../mocks/SwapQuoterMock.sol"; +import {WETHMock} from "../mocks/WETHMock.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract SynapseIntentPreviewerTest is Test { + address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint256 internal constant AMOUNT_IN = 1.337 ether; + uint256 internal constant SWAP_AMOUNT_OUT = 4.2 ether; + uint256 internal constant ALL_ACTIONS_MASK = type(uint256).max; + uint256 internal constant FULL_AMOUNT = type(uint256).max; + + uint8 internal constant TOKEN_IN_INDEX = 2; + uint8 internal constant TOKEN_OUT_INDEX = 1; + uint8 internal constant TOKENS = 3; + uint8 internal constant LP_TOKEN_INDEX = type(uint8).max; + + ZapDataV1Harness internal zapDataLib; + + SynapseIntentPreviewer internal sip; + address internal defaultPoolMock; + address internal swapQuoterMock; + + address internal weth; + address internal tokenA; + address internal tokenB; + address internal lpToken; + + address internal routerAdapterMock = makeAddr("Router Adapter Mock"); + + function setUp() public { + sip = new SynapseIntentPreviewer(); + + defaultPoolMock = address(new DefaultPoolMock()); + swapQuoterMock = address(new SwapQuoterMock()); + + weth = address(new WETHMock()); + tokenA = address(new MockERC20("A", 18)); + tokenB = address(new MockERC20("B", 18)); + lpToken = address(new MockERC20("LP", 18)); + + zapDataLib = new ZapDataV1Harness(); + + vm.label(defaultPoolMock, "DefaultPoolMock"); + vm.label(swapQuoterMock, "SwapQuoterMock"); + vm.label(weth, "WETHMock"); + vm.label(tokenA, "TokenA"); + vm.label(tokenB, "TokenB"); + vm.label(lpToken, "LPToken"); + vm.label(address(zapDataLib), "ZapDataV1Harness"); + + vm.mockCall({ + callee: defaultPoolMock, + data: abi.encodeCall(DefaultPoolMock.swapStorage, ()), + returnData: abi.encode(0, 0, 0, 0, 0, 0, lpToken) + }); + } + + function mockGetAmountOut(address tokenIn, address tokenOut, uint256 amountIn, SwapQuery memory mockQuery) public { + LimitedToken memory token = LimitedToken({actionMask: ALL_ACTIONS_MASK, token: tokenIn}); + vm.mockCall({ + callee: swapQuoterMock, + data: abi.encodeCall(SwapQuoterMock.getAmountOut, (token, tokenOut, amountIn)), + returnData: abi.encode(mockQuery) + }); + } + + function mockGetToken(uint8 tokenIndex, address token) public { + vm.mockCall({ + callee: defaultPoolMock, + data: abi.encodeCall(DefaultPoolMock.getToken, (tokenIndex)), + returnData: abi.encode(token) + }); + } + + function getSwapQuery(address tokenOut) public view returns (SwapQuery memory) { + return SwapQuery({ + routerAdapter: routerAdapterMock, + tokenOut: tokenOut, + minAmountOut: SWAP_AMOUNT_OUT, + deadline: type(uint256).max, + rawParams: abi.encode( + DefaultParams({ + action: Action.Swap, + pool: defaultPoolMock, + tokenIndexFrom: TOKEN_IN_INDEX, + tokenIndexTo: TOKEN_OUT_INDEX + }) + ) + }); + } + + function getSwapZapData() public view returns (bytes memory) { + return getSwapZapData(TOKEN_IN_INDEX, TOKEN_OUT_INDEX); + } + + function getSwapZapData(uint8 indexIn, uint8 indexOut) public view returns (bytes memory) { + return zapDataLib.encodeV1({ + target_: defaultPoolMock, + // 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 + amountPosition_: 4 + 32 * 2 + }); + } + + function test_getSwapZapData() public view { + for (uint8 i = 0; i < TOKENS; i++) { + for (uint8 j = 0; j < TOKENS; j++) { + bytes memory zapData = getSwapZapData(i, j); + 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))); + } + } + } + + function getAddLiquidityQuery(address tokenOut) public view returns (SwapQuery memory) { + return SwapQuery({ + routerAdapter: routerAdapterMock, + tokenOut: tokenOut, + minAmountOut: SWAP_AMOUNT_OUT, + deadline: type(uint256).max, + rawParams: abi.encode( + DefaultParams({ + action: Action.AddLiquidity, + pool: defaultPoolMock, + tokenIndexFrom: TOKEN_IN_INDEX, + tokenIndexTo: LP_TOKEN_INDEX + }) + ) + }); + } + + function getAddLiquidityZapData() public view returns (bytes memory) { + return getAddLiquidityZapData(TOKEN_IN_INDEX); + } + + function getAddLiquidityZapData(uint8 indexIn) public view returns (bytes memory) { + uint256[] memory amounts = new uint256[](TOKENS); + return zapDataLib.encodeV1({ + target_: defaultPoolMock, + // 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 + // (amounts.offset, minToMint, deadline, amounts.length) + amountPosition_: 4 + 32 * (4 + indexIn) + }); + } + + function test_getAddLiquidityZapData() public view { + for (uint8 i = 0; i < TOKENS; i++) { + bytes memory zapData = getAddLiquidityZapData(i); + 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))); + } + } + + function getRemoveLiquidityQuery(address tokenOut) public view returns (SwapQuery memory) { + return SwapQuery({ + routerAdapter: routerAdapterMock, + tokenOut: tokenOut, + minAmountOut: SWAP_AMOUNT_OUT, + deadline: type(uint256).max, + rawParams: abi.encode( + DefaultParams({ + action: Action.RemoveLiquidity, + pool: defaultPoolMock, + tokenIndexFrom: LP_TOKEN_INDEX, + tokenIndexTo: TOKEN_OUT_INDEX + }) + ) + }); + } + + function getRemoveLiquidityZapData() public view returns (bytes memory) { + return getRemoveLiquidityZapData(TOKEN_OUT_INDEX); + } + + function getRemoveLiquidityZapData(uint8 indexOut) public view returns (bytes memory) { + return zapDataLib.encodeV1({ + target_: defaultPoolMock, + // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) + payload_: abi.encodeCall(IDefaultExtendedPool.removeLiquidityOneToken, (0, indexOut, 0, type(uint256).max)), + // Amount (tokenAmount) is encoded as the first parameter + amountPosition_: 4 + }); + } + + function test_getRemoveLiquidityZapData() public view { + for (uint8 i = 0; i < TOKENS; i++) { + bytes memory zapData = getRemoveLiquidityZapData(i); + 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)) + ); + } + } + + function getWrapETHQuery(address tokenOut) public view returns (SwapQuery memory) { + return SwapQuery({ + routerAdapter: routerAdapterMock, + tokenOut: tokenOut, + minAmountOut: AMOUNT_IN, + deadline: type(uint256).max, + rawParams: abi.encode( + DefaultParams({ + action: Action.HandleEth, + pool: address(0), + tokenIndexFrom: LP_TOKEN_INDEX, + tokenIndexTo: LP_TOKEN_INDEX + }) + ) + }); + } + + function getWrapETHZapData() public view returns (bytes memory) { + return zapDataLib.encodeV1({ + target_: weth, + // deposit() + payload_: abi.encodeCall(WETHMock.deposit, ()), + // Amount is not encoded + amountPosition_: zapDataLib.AMOUNT_NOT_PRESENT() + }); + } + + function test_getWrapETHZapData() public view { + bytes memory zapData = getWrapETHZapData(); + bytes memory payload = zapDataLib.payload(zapData, AMOUNT_IN); + // deposit() + assertEq(payload, abi.encodeCall(WETHMock.deposit, ())); + } + + function getUnwrapWETHQuery(address tokenOut) public view returns (SwapQuery memory) { + return SwapQuery({ + routerAdapter: routerAdapterMock, + tokenOut: tokenOut, + minAmountOut: AMOUNT_IN, + deadline: type(uint256).max, + rawParams: abi.encode( + DefaultParams({ + action: Action.HandleEth, + pool: address(0), + tokenIndexFrom: LP_TOKEN_INDEX, + tokenIndexTo: LP_TOKEN_INDEX + }) + ) + }); + } + + function getUnwrapWETHZapData() public view returns (bytes memory) { + return zapDataLib.encodeV1({ + target_: weth, + // withdraw(amount) + payload_: abi.encodeCall(WETHMock.withdraw, (0)), + // Amount is encoded as the first parameter + amountPosition_: 4 + }); + } + + function test_getUnwrapWETHZapData() public view { + bytes memory zapData = getUnwrapWETHZapData(); + bytes memory payload = zapDataLib.payload(zapData, AMOUNT_IN); + // withdraw(amount) + assertEq(payload, abi.encodeCall(WETHMock.withdraw, (AMOUNT_IN))); + } + + function assertEq(ISynapseIntentRouter.StepParams memory a, ISynapseIntentRouter.StepParams memory b) public pure { + assertEq(a.token, b.token); + assertEq(a.amount, b.amount); + assertEq(a.msgValue, b.msgValue); + assertEq(a.zapData, b.zapData); + } + + // ════════════════════════════════════════════════ 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}); + // Checks + assertEq(amountOut, AMOUNT_IN); + assertEq(steps.length, 0); + } + + function test_previewIntent_noOp_native() public view { + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = sip.previewIntent({ + swapQuoter: swapQuoterMock, + tokenIn: NATIVE_GAS_TOKEN, + tokenOut: NATIVE_GAS_TOKEN, + amountIn: AMOUNT_IN + }); + // Checks + assertEq(amountOut, AMOUNT_IN); + assertEq(steps.length, 0); + } + + 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}); + // Checks + assertEq(amountOut, 0); + assertEq(steps.length, 0); + } + + // ════════════════════════════════════════════════ SINGLE STEP ════════════════════════════════════════════════════ + + function checkSingleStepIntent( + address tokenIn, + address tokenOut, + uint256 expectedAmountOut, + ISynapseIntentRouter.StepParams memory expectedStep + ) + public + view + { + // Preview intent + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = + sip.previewIntent({swapQuoter: swapQuoterMock, tokenIn: tokenIn, tokenOut: tokenOut, amountIn: AMOUNT_IN}); + // Checks + assertEq(amountOut, expectedAmountOut); + assertEq(steps.length, 1); + assertEq(steps[0], expectedStep); + } + + function test_previewIntent_swap() public { + SwapQuery memory mockQuery = getSwapQuery(tokenB); + mockGetToken(TOKEN_IN_INDEX, tokenA); + mockGetToken(TOKEN_OUT_INDEX, tokenB); + mockGetAmountOut({tokenIn: tokenA, tokenOut: tokenB, amountIn: AMOUNT_IN, mockQuery: mockQuery}); + ISynapseIntentRouter.StepParams memory expectedStep = ISynapseIntentRouter.StepParams({ + token: tokenA, + amount: FULL_AMOUNT, + msgValue: 0, + zapData: getSwapZapData() + }); + checkSingleStepIntent(tokenA, tokenB, SWAP_AMOUNT_OUT, expectedStep); + } + + function test_previewIntent_addLiquidity() public { + SwapQuery memory mockQuery = getAddLiquidityQuery(lpToken); + mockGetToken(TOKEN_IN_INDEX, tokenA); + mockGetAmountOut({tokenIn: tokenA, tokenOut: lpToken, amountIn: AMOUNT_IN, mockQuery: mockQuery}); + ISynapseIntentRouter.StepParams memory expectedStep = ISynapseIntentRouter.StepParams({ + token: tokenA, + amount: FULL_AMOUNT, + msgValue: 0, + zapData: getAddLiquidityZapData() + }); + checkSingleStepIntent(tokenA, lpToken, SWAP_AMOUNT_OUT, expectedStep); + } + + function test_previewIntent_removeLiquidity() public { + SwapQuery memory mockQuery = getRemoveLiquidityQuery(tokenB); + mockGetToken(TOKEN_OUT_INDEX, tokenB); + mockGetAmountOut({tokenIn: lpToken, tokenOut: tokenB, amountIn: AMOUNT_IN, mockQuery: mockQuery}); + ISynapseIntentRouter.StepParams memory expectedStep = ISynapseIntentRouter.StepParams({ + token: lpToken, + amount: FULL_AMOUNT, + msgValue: 0, + zapData: getRemoveLiquidityZapData() + }); + checkSingleStepIntent(lpToken, tokenB, SWAP_AMOUNT_OUT, expectedStep); + } + + function test_previewIntent_wrapETH() 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() + }); + checkSingleStepIntent(NATIVE_GAS_TOKEN, weth, AMOUNT_IN, expectedStep); + } + + function test_previewIntent_unwrapETH() 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() + }); + checkSingleStepIntent(weth, NATIVE_GAS_TOKEN, AMOUNT_IN, expectedStep); + } + + // ════════════════════════════════════════════════ DOUBLE STEP ════════════════════════════════════════════════════ + + function checkDoubleStepIntent( + address tokenIn, + address tokenOut, + uint256 expectedAmountOut, + ISynapseIntentRouter.StepParams memory expectedStep0, + ISynapseIntentRouter.StepParams memory expectedStep1 + ) + public + view + { + // Preview intent + (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) = + sip.previewIntent({swapQuoter: swapQuoterMock, tokenIn: tokenIn, tokenOut: tokenOut, amountIn: AMOUNT_IN}); + // Checks + assertEq(amountOut, expectedAmountOut); + assertEq(steps.length, 2); + assertEq(steps[0], expectedStep0); + assertEq(steps[1], expectedStep1); + } + + function test_previewIntent_swap_unwrapWETH() 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 + ISynapseIntentRouter.StepParams memory expectedStep0 = ISynapseIntentRouter.StepParams({ + token: tokenA, + amount: FULL_AMOUNT, + msgValue: 0, + zapData: getSwapZapData() + }); + // step1: weth -> NATIVE_GAS_TOKEN + ISynapseIntentRouter.StepParams memory expectedStep1 = ISynapseIntentRouter.StepParams({ + token: weth, + amount: FULL_AMOUNT, + msgValue: 0, + zapData: getUnwrapWETHZapData() + }); + checkDoubleStepIntent(tokenA, NATIVE_GAS_TOKEN, SWAP_AMOUNT_OUT, expectedStep0, expectedStep1); + } + + function test_previewIntent_wrapETH_swap() 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 + ISynapseIntentRouter.StepParams memory expectedStep0 = ISynapseIntentRouter.StepParams({ + token: NATIVE_GAS_TOKEN, + amount: FULL_AMOUNT, + msgValue: AMOUNT_IN, + zapData: getWrapETHZapData() + }); + // 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); + } +} From f3404ccce0cc2613732af95a1fcfa02ee25590d9 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:09:46 +0000 Subject: [PATCH 13/15] feat: Synapse Intent Previewer --- .../legacy/router/interfaces/IWETH9.sol | 2 +- .../router/SynapseIntentPreviewer.sol | 300 +++++++++++++++++- 2 files changed, 299 insertions(+), 3 deletions(-) diff --git a/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol b/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol index a451ff5134..2eab02750f 100644 --- a/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol +++ b/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; interface IWETH9 { function deposit() external payable; diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol index aeef811abe..caec160b36 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol @@ -3,9 +3,30 @@ pragma solidity 0.8.24; // ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ +import {ISynapseIntentPreviewer} from "../interfaces/ISynapseIntentPreviewer.sol"; import {ISynapseIntentRouter} from "../interfaces/ISynapseIntentRouter.sol"; +import {ISwapQuoter} from "../legacy/rfq/interfaces/ISwapQuoter.sol"; +import {IDefaultExtendedPool, IDefaultPool} from "../legacy/router/interfaces/IDefaultExtendedPool.sol"; +import {IWETH9} from "../legacy/router/interfaces/IWETH9.sol"; -contract SynapseIntentPreviewer { +// ═════════════════════════════════════════════ INTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {Action, DefaultParams, LimitedToken, SwapQuery} from "../legacy/router/libs/Structs.sol"; +import {ZapDataV1} from "../libs/ZapDataV1.sol"; + +contract SynapseIntentPreviewer is ISynapseIntentPreviewer { + /// @notice The address reserved for the native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc.). + address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /// @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__PoolTokenMismatch(); + error SIP__PoolZeroAddress(); + error SIP__RawParamsEmpty(); + error SIP__TokenNotNative(); + + /// @inheritdoc ISynapseIntentPreviewer function previewIntent( address swapQuoter, address tokenIn, @@ -16,6 +37,281 @@ contract SynapseIntentPreviewer { view returns (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) { - // TODO: implement + // First, check if the intent is a no-op. + if (tokenIn == tokenOut) { + return (amountIn, new ISynapseIntentRouter.StepParams[](0)); + } + + // Obtain the swap quote, don't put any restrictions on the actions allowed to complete the intent. + SwapQuery memory query = ISwapQuoter(swapQuoter).getAmountOut( + LimitedToken({token: tokenIn, actionMask: type(uint256).max}), tokenOut, amountIn + ); + + // Check if a quote was returned. + amountOut = query.minAmountOut; + if (amountOut == 0) { + return (0, new ISynapseIntentRouter.StepParams[](0)); + } + + // At this point we have a quote for a non-trivial action, therefore `query.rawParams` is not empty. + if (query.rawParams.length == 0) revert SIP__RawParamsEmpty(); + DefaultParams memory params = abi.decode(query.rawParams, (DefaultParams)); + + // Create the steps for the intent based on the action type. + if (params.action == Action.Swap) { + steps = _createSwapSteps(tokenIn, tokenOut, amountIn, params); + } else if (params.action == Action.AddLiquidity) { + steps = _createAddLiquiditySteps(tokenIn, tokenOut, params); + } else if (params.action == Action.RemoveLiquidity) { + steps = _createRemoveLiquiditySteps(tokenIn, tokenOut, params); + } else { + steps = _createHandleHativeSteps(tokenIn, tokenOut, amountIn); + } + } + + /// @notice Helper function to create steps for a swap. + function _createSwapSteps( + address tokenIn, + address tokenOut, + uint256 amountIn, + DefaultParams memory params + ) + internal + view + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + address pool = params.pool; + if (pool == address(0)) revert SIP__PoolZeroAddress(); + // Default Pools can only host wrapped native tokens. + // Check if we start from the native gas token. + if (tokenIn == NATIVE_GAS_TOKEN) { + // Get the address of the wrapped native token. + 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. + return _toStepsArray( + _createWrapNativeStep({wrappedNative: wrappedNative, amountIn: amountIn}), + _createSwapStep({tokenIn: wrappedNative, params: params}) + ); + } + + // Sanity check tokenIn vs tokenIndexFrom. + if (IDefaultPool(pool).getToken(params.tokenIndexFrom) != tokenIn) revert SIP__PoolTokenMismatch(); + + // Check if we end with the native gas token. + if (tokenOut == NATIVE_GAS_TOKEN) { + // Get the address of the wrapped native token. + address wrappedNative = IDefaultPool(pool).getToken(params.tokenIndexTo); + // TokenIn => WrappedNative + WrappedNative => Native. + return _toStepsArray( + _createSwapStep({tokenIn: tokenIn, params: params}), + _createUnwrapNativeStep({wrappedNative: wrappedNative}) + ); + } + + // Sanity check tokenOut vs tokenIndexTo. + if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch(); + + // TokenIn => TokenOut. + return _toStepsArray(_createSwapStep({tokenIn: tokenIn, params: params})); + } + + /// @notice Helper function to create steps for adding liquidity. + function _createAddLiquiditySteps( + address tokenIn, + address tokenOut, + DefaultParams memory params + ) + internal + view + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + address pool = params.pool; + if (pool == address(0)) revert SIP__PoolZeroAddress(); + // Sanity check tokenIn vs tokenIndexFrom. + if (IDefaultPool(pool).getToken(params.tokenIndexFrom) != tokenIn) revert SIP__PoolTokenMismatch(); + // Sanity check tokenOut vs pool's LP token. + _verifyLpToken(pool, tokenOut); + // Figure out how many tokens does the pool support. + uint256[] memory amounts; + for (uint8 i = 0;; i++) { + // solhint-disable-next-line no-empty-blocks + try IDefaultExtendedPool(pool).getToken(i) returns (address) { + // Token exists, continue. + } catch { + // No more tokens, allocate the array using the correct size. + amounts = new uint256[](i); + break; + } + } + return _toStepsArray( + ISynapseIntentRouter.StepParams({ + token: tokenIn, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: pool, + // 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 + // (amounts.offset, minToMint, deadline, amounts.length). + amountPosition_: 4 + 32 * 4 + 32 * uint16(params.tokenIndexFrom) + }) + }) + ); + } + + /// @notice Helper function to create steps for removing liquidity. + function _createRemoveLiquiditySteps( + address tokenIn, + address tokenOut, + DefaultParams memory params + ) + internal + view + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + address pool = params.pool; + if (pool == address(0)) revert SIP__PoolZeroAddress(); + // Sanity check tokenIn vs pool's LP token. + _verifyLpToken(pool, tokenIn); + // Sanity check tokenOut vs tokenIndexTo. + if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch(); + return _toStepsArray( + ISynapseIntentRouter.StepParams({ + token: tokenIn, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: pool, + // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) + payload_: abi.encodeCall( + IDefaultExtendedPool.removeLiquidityOneToken, (0, params.tokenIndexTo, 0, type(uint256).max) + ), + // amountIn is encoded as the first parameter: tokenAmount + amountPosition_: 4 + }) + }) + ); + } + + function _verifyLpToken(address pool, address token) internal view { + (,,,,,, address lpToken) = IDefaultExtendedPool(pool).swapStorage(); + if (lpToken != token) revert SIP__PoolTokenMismatch(); + } + + /// @notice Helper function to create steps for wrapping or unwrapping native gas tokens. + function _createHandleHativeSteps( + address tokenIn, + address tokenOut, + uint256 amountIn + ) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + if (tokenIn == NATIVE_GAS_TOKEN) { + // tokenOut is Wrapped Native + return _toStepsArray(_createWrapNativeStep({wrappedNative: tokenOut, amountIn: amountIn})); + } + // Sanity check tokenOut + if (tokenOut != NATIVE_GAS_TOKEN) revert SIP__TokenNotNative(); + // tokenIn is Wrapped Native + return _toStepsArray(_createUnwrapNativeStep({wrappedNative: tokenIn})); + } + + /// @notice Helper function to create a single step for a swap. + function _createSwapStep( + address tokenIn, + DefaultParams memory params + ) + internal + pure + returns (ISynapseIntentRouter.StepParams memory) + { + return ISynapseIntentRouter.StepParams({ + token: tokenIn, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: params.pool, + // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) + payload_: abi.encodeCall( + IDefaultPool.swap, (params.tokenIndexFrom, params.tokenIndexTo, 0, 0, type(uint256).max) + ), + // amountIn is encoded as the third parameter: `dx` + amountPosition_: 4 + 32 * 2 + }) + }); + } + + /// @notice Helper function to create a single step for wrapping native gas tokens. + function _createWrapNativeStep( + address wrappedNative, + uint256 amountIn + ) + internal + pure + returns (ISynapseIntentRouter.StepParams memory) + { + return ISynapseIntentRouter.StepParams({ + token: NATIVE_GAS_TOKEN, + amount: FULL_BALANCE, + msgValue: amountIn, + zapData: ZapDataV1.encodeV1({ + target_: wrappedNative, + // deposit() + payload_: abi.encodeCall(IWETH9.deposit, ()), + // amountIn is not encoded + amountPosition_: ZapDataV1.AMOUNT_NOT_PRESENT + }) + }); + } + + /// @notice Helper function to create a single step for unwrapping native gas tokens. + function _createUnwrapNativeStep(address wrappedNative) + internal + pure + returns (ISynapseIntentRouter.StepParams memory) + { + return ISynapseIntentRouter.StepParams({ + token: wrappedNative, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: wrappedNative, + // withdraw(amount) + payload_: abi.encodeCall(IWETH9.withdraw, (0)), + // amountIn encoded as the first parameter + amountPosition_: 4 + }) + }); + } + + /// @notice Helper function to construct an array of steps having a single step. + function _toStepsArray(ISynapseIntentRouter.StepParams memory step0) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory) + { + ISynapseIntentRouter.StepParams[] memory steps = new ISynapseIntentRouter.StepParams[](1); + steps[0] = step0; + return steps; + } + + /// @notice Helper function to construct an array of steps having two steps. + function _toStepsArray( + ISynapseIntentRouter.StepParams memory step0, + ISynapseIntentRouter.StepParams memory step1 + ) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory) + { + ISynapseIntentRouter.StepParams[] memory steps = new ISynapseIntentRouter.StepParams[](2); + steps[0] = step0; + steps[1] = step1; + return steps; } } From 2e6bce6d081469172ba6215d0a419d50b15d9b14 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:39:01 +0000 Subject: [PATCH 14/15] feat: update deploy script, deploy SIP --- .../arbitrum/SynapseIntentPreviewer.json | 114 ++++++++++++++++++ .../optimism/SynapseIntentPreviewer.json | 114 ++++++++++++++++++ packages/contracts-rfq/script/DeploySIR.s.sol | 2 + 3 files changed, 230 insertions(+) create mode 100644 packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json create mode 100644 packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json diff --git a/packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json b/packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json new file mode 100644 index 0000000000..fb6bdf01ec --- /dev/null +++ b/packages/contracts-rfq/deployments/arbitrum/SynapseIntentPreviewer.json @@ -0,0 +1,114 @@ +{ + "address": "0xE184826D4aBC2798134abE7e2Fd72156827Fc7EA", + "constructorArgs": "0x", + "receipt": { + "hash": "0xdee35d80aa7aa61e76d152e22676067769f2923c85ed5e96d55c807ac9418277", + "blockNumber": 281941808 + }, + "abi": [ + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewIntent", + "inputs": [ + { + "name": "swapQuoter", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenIn", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenOut", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "amountOut", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "error", + "name": "SIP__PoolTokenMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "SIP__PoolZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "SIP__RawParamsEmpty", + "inputs": [] + }, + { + "type": "error", + "name": "SIP__TokenNotNative", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__InvalidEncoding", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__TargetZeroAddress", + "inputs": [] + } + ] +} \ No newline at end of file diff --git a/packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json b/packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json new file mode 100644 index 0000000000..e53f8622ad --- /dev/null +++ b/packages/contracts-rfq/deployments/optimism/SynapseIntentPreviewer.json @@ -0,0 +1,114 @@ +{ + "address": "0xE184826D4aBC2798134abE7e2Fd72156827Fc7EA", + "constructorArgs": "0x", + "receipt": { + "hash": "0x57d5d445810008804844d57d54ab99ab5229e87a206996f9cafc67494bf7fd77", + "blockNumber": 128944965 + }, + "abi": [ + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewIntent", + "inputs": [ + { + "name": "swapQuoter", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenIn", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenOut", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "amountOut", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "error", + "name": "SIP__PoolTokenMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "SIP__PoolZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "SIP__RawParamsEmpty", + "inputs": [] + }, + { + "type": "error", + "name": "SIP__TokenNotNative", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__InvalidEncoding", + "inputs": [] + }, + { + "type": "error", + "name": "ZapDataV1__TargetZeroAddress", + "inputs": [] + } + ] +} \ No newline at end of file diff --git a/packages/contracts-rfq/script/DeploySIR.s.sol b/packages/contracts-rfq/script/DeploySIR.s.sol index 0cd16705be..f15352037c 100644 --- a/packages/contracts-rfq/script/DeploySIR.s.sol +++ b/packages/contracts-rfq/script/DeploySIR.s.sol @@ -6,6 +6,7 @@ import {SynapseScript} from "@synapsecns/solidity-devops/src/SynapseScript.sol"; // solhint-disable no-empty-blocks contract DeploySIR is SynapseScript { string public constant LATEST_SIR = "SynapseIntentRouter"; + string public constant LATEST_SIP = "SynapseIntentPreviewer"; string public constant LATEST_ZAP = "TokenZapV1"; /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. @@ -14,6 +15,7 @@ contract DeploySIR is SynapseScript { function run() external broadcastWithHooks { // TODO: create2 salts deployAndSave({contractName: LATEST_SIR, constructorArgs: "", deployCodeFunc: cbDeployCreate2}); + deployAndSave({contractName: LATEST_SIP, constructorArgs: "", deployCodeFunc: cbDeployCreate2}); deployAndSave({contractName: LATEST_ZAP, constructorArgs: "", deployCodeFunc: cbDeployCreate2}); } } From e28e56e692dacda791375b0212b40c3d616c253a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:27:42 +0000 Subject: [PATCH 15/15] fix(contracts-rfq): add `forwardTo` to ZapData for Zap Actions that don't forward assets to the user (#3451) * feat: scaffold finalToken, forwardTo in ZapDataV1 * feat: add finalToken, forwardTo * feat: scaffold TokenZap.encodeZapData * test: update existing tests * test: forwardTo scenarios * feat: token zeor address checks * feat:scaffold token forwarding * test: more revert cases * feat: final token forwarding * test: forwardTo behaviour in SIP * feat: support optional `forwardTo` in SIP * deploy: redeploy SIP, TokenZap --- .../interfaces/ISynapseIntentPreviewer.sol | 3 + .../contracts/libs/ZapDataV1.sol | 40 +- .../router/SynapseIntentPreviewer.sol | 70 +++- .../contracts/zaps/TokenZapV1.sol | 46 +- .../arbitrum/SynapseIntentPreviewer.json | 16 +- .../deployments/arbitrum/TokenZapV1.json | 26 +- .../optimism/SynapseIntentPreviewer.json | 16 +- .../deployments/optimism/TokenZapV1.json | 26 +- .../test/harnesses/ZapDataV1Harness.sol | 12 +- .../test/integration/TokenZapV1.t.sol | 20 +- .../contracts-rfq/test/libs/ZapDataV1.t.sol | 55 ++- .../test/router/SynapseIntentPreviewer.t.sol | 298 ++++++++++--- .../SynapseIntentRouter.BalanceChecks.t.sol | 51 +++ .../test/router/SynapseIntentRouter.t.sol | 360 +++++++++++++++- .../test/zaps/TokenZapV1.GasBench.t.sol | 2 +- .../contracts-rfq/test/zaps/TokenZapV1.t.sol | 392 ++++++++++++------ 16 files changed, 1179 insertions(+), 254 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/libs/ZapDataV1.sol b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol index 0b7c13a9d1..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(); @@ -44,6 +48,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 +63,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_ ) @@ -63,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. @@ -74,6 +88,22 @@ library ZapDataV1 { } } + /// @notice Extracts the finalToken address from the encoded Zap Data. + function finalToken(bytes calldata encodedZapData) internal pure returns (address finalToken_) { + // 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_) { + // 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. 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..870a86c3a6 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol @@ -21,14 +21,17 @@ 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(); error SIP__TokenNotNative(); /// @inheritdoc ISynapseIntentPreviewer + // solhint-disable-next-line code-complexity function previewIntent( address swapQuoter, + address forwardTo, address tokenIn, address tokenOut, uint256 amountIn @@ -39,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)); } @@ -59,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); } } @@ -74,7 +78,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { address tokenIn, address tokenOut, uint256 amountIn, - DefaultParams memory params + DefaultParams memory params, + address forwardTo ) internal view @@ -89,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}) ); } @@ -103,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}) ); } @@ -114,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 @@ -152,6 +160,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: pool, + 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 @@ -166,7 +176,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { function _createRemoveLiquiditySteps( address tokenIn, address tokenOut, - DefaultParams memory params + DefaultParams memory params, + address forwardTo ) internal view @@ -185,6 +196,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: pool, + finalToken_: tokenOut, + forwardTo_: forwardTo, // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) payload_: abi.encodeCall( IDefaultExtendedPool.removeLiquidityOneToken, (0, params.tokenIndexTo, 0, type(uint256).max) @@ -205,7 +218,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { function _createHandleHativeSteps( address tokenIn, address tokenOut, - uint256 amountIn + uint256 amountIn, + address forwardTo ) internal pure @@ -213,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 @@ -236,6 +254,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: params.pool, + finalToken_: tokenOut, + forwardTo_: forwardTo, // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) payload_: abi.encodeCall( IDefaultPool.swap, (params.tokenIndexFrom, params.tokenIndexTo, 0, 0, type(uint256).max) @@ -249,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 @@ -258,9 +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, + finalToken_: wrappedNative, + forwardTo_: forwardTo, // deposit() payload_: abi.encodeCall(IWETH9.deposit, ()), // amountIn is not encoded @@ -270,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) @@ -281,6 +307,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer { msgValue: 0, zapData: ZapDataV1.encodeV1({ target_: wrappedNative, + finalToken_: NATIVE_GAS_TOKEN, + forwardTo_: forwardTo, // 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..0e0e7859dc 100644 --- a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -26,8 +26,10 @@ contract TokenZapV1 is IZapRecipient { address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + error TokenZapV1__FinalTokenBalanceZero(); 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. @@ -46,6 +48,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(); @@ -81,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; } @@ -100,10 +108,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 @@ -112,6 +130,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. @@ -119,7 +141,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. - return ZapDataV1.encodeV1(uint16(amountPosition), 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, @@ -138,4 +166,18 @@ contract TokenZapV1 is IZapRecipient { target = zapData.target(); payload = zapData.payload(amount); } + + /// @notice Forwards the proceeds of the Zap action to the specified non-zero recipient. + function _forwardToken(address token, address forwardTo) internal { + // 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); + } + } } 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", 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/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/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..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,13 +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, + 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 @@ -117,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, @@ -145,14 +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, + 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 @@ -161,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, @@ -189,13 +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, + 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 @@ -203,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, @@ -232,9 +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, + finalToken_: weth, + forwardTo_: forwardTo, // deposit() payload_: abi.encodeCall(WETHMock.deposit, ()), // Amount is not encoded @@ -242,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) { @@ -266,9 +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, + finalToken_: NATIVE_GAS_TOKEN, + forwardTo_: forwardTo, // withdraw(amount) payload_: abi.encodeCall(WETHMock.withdraw, (0)), // Amount is encoded as the first parameter @@ -276,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 { @@ -293,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 @@ -312,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); @@ -330,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); @@ -353,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}); @@ -366,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}); @@ -379,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 ════════════════════════════════════════════════════ @@ -415,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); @@ -430,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); } } 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 110be2f523..35501041d3 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) }); } @@ -417,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( @@ -433,7 +777,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 +1170,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 e081831372..5842b25172 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,52 @@ 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 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: finalToken, + 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 test_zap_withdraw_depositNative_placeholderZero() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataDeposit = getZapDataNoAmount(getVaultPayloadNoAmount()); + 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: finalToken, + 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 +231,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 +245,176 @@ 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, nativeGasToken, 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, nativeGasToken, 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(); + deal(address(tokenZap), AMOUNT); + // 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_withdraw_transferNativeEOA_placeholderZero_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_placeholderZero(); + // 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, 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 + assertEq(payableMock.balance, AMOUNT); } - function test_zap_withdraw_transferNativeEOA_placeholderNonZero_extraTokens() public { + function test_zap_unwrapForwardNativeContract_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + 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 + assertEq(payableMock.balance, AMOUNT); + } + + function test_zap_unwrapForwardNativeContract_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_placeholderNonZero(); + deal(address(tokenZap), AMOUNT); + // 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_withdraw_transferNativeEOA_placeholderNonZero_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_transferNativeEOA_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_withdraw_transferNativeContract_placeholderZero() public { - bytes memory zapDataWithdraw = getZapDataWithdraw(0); - bytes memory zapDataTransfer = tokenZap.encodeZapData({target: payableMock, payload: "", amountPosition: 0}); - weth.transfer(address(tokenZap), AMOUNT); + 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(address(weth), AMOUNT, zapDataWithdraw); + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapDataWrap); assertEq(returnValue, tokenZap.zap.selector); - returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataDeposit); assertEq(returnValue, tokenZap.zap.selector); - // Check that the contract received the native tokens - assertEq(payableMock.balance, AMOUNT); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); } - function test_zap_withdraw_transferNativeContract_placeholderNonZero() public { + function test_zap_wrap_depositWETH_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}); - weth.transfer(address(tokenZap), AMOUNT); + bytes memory zapDataWrap = getZapDataWrap(); + bytes memory zapDataDeposit = getZapDataDeposit(getVaultPayload(address(weth), 1 ether)); // Do two Zaps in a row - bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapDataWrap); assertEq(returnValue, tokenZap.zap.selector); - returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataDeposit); assertEq(returnValue, tokenZap.zap.selector); - // Check that the contract received the native tokens - assertEq(payableMock.balance, AMOUNT); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(weth)), AMOUNT); } - function test_zap_withdraw_transferNativeContract_placeholderZero_extraTokens() public { + 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_placeholderZero(); + test_zap_wrap_depositWETH_placeholderZero(); } - function test_zap_withdraw_transferNativeContract_placeholderZero_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_placeholderZero(); + test_zap_wrap_depositWETH_placeholderNonZero(); } - function test_zap_withdraw_transferNativeContract_placeholderNonZero_extraTokens() public { - // Transfer some extra tokens to the zap contract - weth.transfer(address(tokenZap), AMOUNT); - // Should not affect the zap - test_zap_withdraw_transferNativeContract_placeholderNonZero(); + function test_zap_wrapForward() public { + 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_withdraw_transferNativeContract_placeholderNonZero_extraNative() public { - // Transfer some extra native tokens to the zap contract + function test_zap_wrapForward_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(); + // 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 ══════════════════════════════════════════════════════ @@ -353,7 +423,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 +435,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); @@ -375,11 +445,25 @@ 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 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 = 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 +471,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 +499,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 +510,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 +524,111 @@ 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, nativeGasToken, 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_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); - tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + 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({ + target: user, + payload: getVaultPayloadNoAmount(), + amountPosition: 0, + finalToken: address(0), + forwardTo: address(0) + }); + weth.transfer(address(tokenZap), AMOUNT); + 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); } }