// SPDX-License-Identifier: MIT pragma solidity 0.8.24; // ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ 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 completeIntentWithBalanceChecks( address zapRecipient, uint256 amountIn, uint256 deadline, StepParams[] calldata steps ) external payable { // 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, 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 function completeIntent( address zapRecipient, uint256 amountIn, uint256 deadline, StepParams[] calldata steps ) public payable { // 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 that we fully spent `msg.value`. 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(); } } }