diff --git a/foundry/interfaces/ISwapExecutor.sol b/foundry/interfaces/ISwapExecutor.sol new file mode 100644 index 0000000..e79a868 --- /dev/null +++ b/foundry/interfaces/ISwapExecutor.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.7.5; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +pragma abicoder v2; + +interface ISwapExecutor { + /** + * @notice Performs a swap on a liquidity pool. + * @dev This method can either take the amount of the input token or the amount + * of the output token that we would like to swap. If called with the amount of + * the input token, the amount of the output token will be returned, and vice + * versa. Whether it is the input or output that is given, is encoded in the data + * parameter. + * + * Note Part of the informal interface is that the executor supports sending the received + * tokens to a receiver address. If the underlying smart contract does not provide this + * functionality consider adding an additional transfer in the implementation. + * + * This function is marked as `payable` to accommodate delegatecalls, which can forward + * a potential `msg.value` to it. + * + * @param givenAmount The amount of either the input token or output token to swap. + * @param data Data that holds information necessary to perform the swap. + * @return calculatedAmount The amount of either the input token or output token + * swapped, depending on the givenAmount inputted. + */ + function swap(uint256 givenAmount, bytes calldata data) + external + payable + returns (uint256 calculatedAmount); +} + +interface ISwapExecutorErrors { + error InvalidParameterLength(uint256); + error UnknownCurveType(uint8); +} diff --git a/foundry/remappings.txt b/foundry/remappings.txt index f1fbdf6..0ab4175 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -1,3 +1,4 @@ @openzeppelin/=lib/openzeppelin-contracts/ +@interfaces/=interfaces/ @permit2/=lib/permit2/ @src/=src/ \ No newline at end of file diff --git a/foundry/src/SwapExecutionDispatcher.sol b/foundry/src/SwapExecutionDispatcher.sol index 3173104..53435d7 100644 --- a/foundry/src/SwapExecutionDispatcher.sol +++ b/foundry/src/SwapExecutionDispatcher.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; +import "@interfaces/ISwapExecutor.sol"; + +error SwapExecutionDispatcher__UnapprovedExecutor(); + /** * @title SwapExecutionDispatcher - Dispatch swap execution to external contracts * @author PropellerHeads Devs @@ -10,8 +14,61 @@ pragma solidity ^0.8.28; * be called using delegatecall so they can share state with the main * contract if needed. * - * Note Executor contracts need to implement the ISwapExecutor interface + * Note Executor contracts need to implement the ISwapExecutor interface unless + * an alternate selector is specified. */ contract SwapExecutionDispatcher { mapping(address => bool) public swapExecutors; + + /** + * @dev Calls an executor, assumes swap.protocolData contains + * token addresses if required by the executor. + */ + function _callSwapExecutor( + uint8 exchange, + uint256 amount, + bytes calldata data + ) internal returns (uint256 calculatedAmount) { + address executor; + bytes4 decodedSelector; + bytes memory protocolData; + + (executor, decodedSelector, protocolData) = + _decodeExecutorAndSelector(data); + + bytes4 selector = decodedSelector == bytes4(0) + ? ISwapExecutor.swap.selector + : decodedSelector; + + if (!swapExecutors[executor]){ + revert SwapExecutionDispatcher__UnapprovedExecutor(); + } + + (bool success, bytes memory result) = executor.delegatecall( + abi.encodeWithSelector(selector, amount, protocolData) + ); + + if (!success) { + revert( + string( + result.length > 0 + ? result + : abi.encodePacked("Swap execution failed") + ) + ); + } + + calculatedAmount = abi.decode(result, (uint256)); + } + + function _decodeExecutorAndSelector(bytes calldata data) + internal + pure + returns (address executor, bytes4 selector, bytes memory protocolData) + { + require(data.length >= 24, "Invalid data length"); + executor = address(uint160(bytes20(data[:20]))); + selector = bytes4(data[20:24]); + protocolData = data[24:]; + } } diff --git a/foundry/test/SwapExecutionDispatcher.t.sol b/foundry/test/SwapExecutionDispatcher.t.sol new file mode 100644 index 0000000..0e81dfb --- /dev/null +++ b/foundry/test/SwapExecutionDispatcher.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "@src/SwapExecutionDispatcher.sol"; +import "./TychoRouterTestSetup.sol"; + + +contract SwapExecutionDispatcherExposed is SwapExecutionDispatcher { + function exposedDecodeExecutorAndSelector(bytes calldata data) + external + pure + returns (address executor, bytes4 selector, bytes memory protocolData) + { + return _decodeExecutorAndSelector(data); + } +} + +contract SwapExecutionDispatcherTest is TychoRouterTestSetup { + SwapExecutionDispatcherExposed dispatcherExposed; + + function setupExecutionDispatcher() public { + dispatcherExposed = new SwapExecutionDispatcherExposed(); + } + + function testDecodeExecutorAndSelector() public { + setupExecutionDispatcher(); + bytes memory data = + hex"6611e616d2db3244244a54c754a16dd3ac7ca7a2aabbccdd1111111111111111"; + (address executor, bytes4 selector, bytes memory protocolData) = + dispatcherExposed.exposedDecodeExecutorAndSelector(data); + assert(executor == address(0x6611e616d2db3244244A54c754A16dd3ac7cA7a2)); + assert(selector == bytes4(0xaabbccdd)); + // Direct bytes comparison not supported - must use keccak + assert(keccak256(protocolData) == keccak256(hex"1111111111111111")); + } +}