feat: delegatecall to executor in SwapExecutionDispatcher
This commit is contained in:
38
foundry/interfaces/ISwapExecutor.sol
Normal file
38
foundry/interfaces/ISwapExecutor.sol
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
@openzeppelin/=lib/openzeppelin-contracts/
|
@openzeppelin/=lib/openzeppelin-contracts/
|
||||||
|
@interfaces/=interfaces/
|
||||||
@permit2/=lib/permit2/
|
@permit2/=lib/permit2/
|
||||||
@src/=src/
|
@src/=src/
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.28;
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import "@interfaces/ISwapExecutor.sol";
|
||||||
|
|
||||||
|
error SwapExecutionDispatcher__UnapprovedExecutor();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title SwapExecutionDispatcher - Dispatch swap execution to external contracts
|
* @title SwapExecutionDispatcher - Dispatch swap execution to external contracts
|
||||||
* @author PropellerHeads Devs
|
* @author PropellerHeads Devs
|
||||||
@@ -10,8 +14,61 @@ pragma solidity ^0.8.28;
|
|||||||
* be called using delegatecall so they can share state with the main
|
* be called using delegatecall so they can share state with the main
|
||||||
* contract if needed.
|
* 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 {
|
contract SwapExecutionDispatcher {
|
||||||
mapping(address => bool) public swapExecutors;
|
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:];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
foundry/test/SwapExecutionDispatcher.t.sol
Normal file
36
foundry/test/SwapExecutionDispatcher.t.sol
Normal file
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user