Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032
MULTISIG_DELEGATOR_IMPLEMENTATION_ADDRESS=
META_SWAP_ADAPTER_OWNER_ADDRESS=
METASWAP_ADDRESS=
SWAPS_API_SIGNER_ADDRESS=
ARGS_EQUALITY_CHECK_ENFORCER_ADDRESS=

# Required for verifying contracts
ETHERSCAN_API_KEY=
Expand Down
4 changes: 0 additions & 4 deletions script/DeployCaveatEnforcers.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { ERC20PeriodTransferEnforcer } from "../src/enforcers/ERC20PeriodTransfe
import { ERC721BalanceGteEnforcer } from "../src/enforcers/ERC721BalanceGteEnforcer.sol";
import { ERC721TransferEnforcer } from "../src/enforcers/ERC721TransferEnforcer.sol";
import { ERC1155BalanceGteEnforcer } from "../src/enforcers/ERC1155BalanceGteEnforcer.sol";
import { ExactCalldataBatchEnforcer } from "../src/enforcers/ExactCalldataBatchEnforcer.sol";
import { ExactCalldataEnforcer } from "../src/enforcers/ExactCalldataEnforcer.sol";
import { ExactExecutionBatchEnforcer } from "../src/enforcers/ExactExecutionBatchEnforcer.sol";
import { ExactExecutionEnforcer } from "../src/enforcers/ExactExecutionEnforcer.sol";
Expand Down Expand Up @@ -104,9 +103,6 @@ contract DeployCaveatEnforcers is Script {
deployedAddress = address(new ERC1155BalanceGteEnforcer{ salt: salt }());
console2.log("ERC1155BalanceGteEnforcer: %s", deployedAddress);

deployedAddress = address(new ExactCalldataBatchEnforcer{ salt: salt }());
console2.log("ExactCalldataBatchEnforcer: %s", deployedAddress);

deployedAddress = address(new ExactCalldataEnforcer{ salt: salt }());
console2.log("ExactCalldataEnforcer: %s", deployedAddress);

Expand Down
11 changes: 9 additions & 2 deletions script/DeployDelegationMetaSwapAdapter.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ contract DeployDelegationMetaSwapAdapter is Script {
bytes32 salt;
address deployer;
address metaSwapAdapterOwner;
address swapApiSignerEnforcer;
IDelegationManager delegationManager;
IMetaSwap metaSwap;
address argsEqualityCheckEnforcer;

function setUp() public {
salt = bytes32(abi.encodePacked(vm.envString("SALT")));
metaSwapAdapterOwner = vm.envAddress("META_SWAP_ADAPTER_OWNER_ADDRESS");
delegationManager = IDelegationManager(vm.envAddress("DELEGATION_MANAGER_ADDRESS"));
metaSwap = IMetaSwap(vm.envAddress("METASWAP_ADDRESS"));
swapApiSignerEnforcer = vm.envAddress("SWAPS_API_SIGNER_ADDRESS");
argsEqualityCheckEnforcer = vm.envAddress("ARGS_EQUALITY_CHECK_ENFORCER_ADDRESS");
deployer = msg.sender;
console2.log("~~~");
console2.log("Deployer: %s", address(deployer));
Expand All @@ -40,8 +44,11 @@ contract DeployDelegationMetaSwapAdapter is Script {
console2.log("~~~");
vm.startBroadcast();

address delegationMetaSwapAdapter =
address(new DelegationMetaSwapAdapter{ salt: salt }(metaSwapAdapterOwner, delegationManager, metaSwap));
address delegationMetaSwapAdapter = address(
new DelegationMetaSwapAdapter{ salt: salt }(
metaSwapAdapterOwner, swapApiSignerEnforcer, delegationManager, metaSwap, argsEqualityCheckEnforcer
)
);
console2.log("DelegationMetaSwapAdapter: %s", delegationMetaSwapAdapter);

vm.stopBroadcast();
Expand Down
160 changes: 145 additions & 15 deletions src/helpers/DelegationMetaSwapAdapter.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;

import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol";
Expand All @@ -15,22 +17,47 @@ import { CALLTYPE_SINGLE, EXECTYPE_DEFAULT } from "../utils/Constants.sol";

/**
* @title DelegationMetaSwapAdapter
* @notice Acts as a middleman to orchestrate token swaps using delegations
* and an aggregator (MetaSwap).
* @notice Acts as a middleman to orchestrate token swaps using delegations and an aggregator (MetaSwap).
* @dev This contract depends on an ArgsEqualityCheckEnforcer. The root delegation must include a caveat
* with this enforcer as its first element. Its arguments indicate whether the swap should enforce the token
* whitelist ("Token-Whitelist-Enforced") or not ("Token-Whitelist-Not-Enforced"). The root delegator is
* responsible for including this enforcer to signal the desired behavior.
*
* @dev This adapter is intended to be used with the Swaps API. Accordingly, all API requests must include a valid
* signature that incorporates an expiration timestamp. The signature is verified during swap execution to ensure
* that it is still valid.
*/
contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
using ModeLib for ModeCode;
using ExecutionLib for bytes;
using SafeERC20 for IERC20;

struct SignatureData {
bytes apiData;
uint256 expiration;
bytes signature;
}

////////////////////////////// State //////////////////////////////

/// @dev Constant value used to enforce the token whitelist
string public constant WHITELIST_ENFORCED = "Token-Whitelist-Enforced";

/// @dev Constant value used to avoid enforcing the token whitelist
string public constant WHITELIST_NOT_ENFORCED = "Token-Whitelist-Not-Enforced";

/// @dev The DelegationManager contract that has root access to this contract
IDelegationManager public immutable delegationManager;

/// @dev The MetaSwap contract used to swap tokens
IMetaSwap public immutable metaSwap;

/// @dev The enforcer used to compare args and terms
address public immutable argsEqualityCheckEnforcer;

/// @dev Address of the API signer account.
address public swapApiSigner;

/// @dev Indicates if a token is allowed to be used in the swaps
mapping(IERC20 token => bool allowed) public isTokenAllowed;

Expand All @@ -45,6 +72,9 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
/// @dev Emitted when the MetaSwap contract address is set.
event SetMetaSwap(IMetaSwap indexed newMetaSwap);

/// @dev Emitted when the Args Equality Check Enforcer contract address is set.
event SetArgsEqualityCheckEnforcer(address indexed newArgsEqualityCheckEnforcer);

/// @dev Emitted when the contract sends tokens (or native tokens) to a recipient.
event SentTokens(IERC20 indexed token, address indexed recipient, uint256 amount);

Expand All @@ -54,6 +84,9 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
/// @dev Emitted when the allowed aggregator ID status changes.
event ChangedAggregatorIdStatus(bytes32 indexed aggregatorIdHash, string aggregatorId, bool status);

/// @dev Emitted when the Signer API is updated.
event SwapApiSignerUpdated(address indexed newSigner);

////////////////////////////// Errors //////////////////////////////

/// @dev Error thrown when the caller is not the delegation manager
Expand All @@ -62,7 +95,7 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
/// @dev Error thrown when the call is not made by this contract itself.
error NotSelf();

/// @dev Error thrown when msg.sender is not the leaf delegatior.
/// @dev Error thrown when msg.sender is not the leaf delegator.
error NotLeafDelegator();

/// @dev Error thrown when an execution with an unsupported CallType is made.
Expand All @@ -87,7 +120,7 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
error TokenToIsNotAllowed(IERC20 token);

/// @dev Error when the aggregator ID is not in the allow list.
error AggregatorIdIsNotAllowed(string);
error AggregatorIdIsNotAllowed(string aggregatorId);

/// @dev Error when the input arrays of a function have different lengths.
error InputLengthsMismatch();
Expand All @@ -104,6 +137,15 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
/// @dev Error when the amountFrom in the api data and swap data do not match.
error AmountFromMismath();

/// @dev Error when the delegations do not include the ArgsEqualityCheckEnforcer
error MissingArgsEqualityCheckEnforcer();

/// @dev Error thrown when API signature is invalid.
error InvalidApiSignature();

/// @dev Error thrown when the signature expiration has passed.
error SignatureExpired();

////////////////////////////// Modifiers //////////////////////////////

/**
Expand All @@ -125,16 +167,31 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
////////////////////////////// Constructor //////////////////////////////

/**
* @notice Initializes the DelegationMetaSwapAdapter contract
* @param _owner The initial owner of the contract
* @param _delegationManager the address of the trusted DelegationManager contract that will have root access to this contract
* @param _metaSwap the address of the trusted MetaSwap contract.
* @notice Initializes the DelegationMetaSwapAdapter contract.
* @param _owner The initial owner of the contract.
* @param _swapApiSigner The initial swap API signer.
* @param _delegationManager The address of the trusted DelegationManager contract has privileged access to call
* executeByExecutor based on a given delegation.
* @param _metaSwap The address of the trusted MetaSwap contract.
* @param _argsEqualityCheckEnforcer The address of the ArgsEqualityCheckEnforcer contract.
*/
constructor(address _owner, IDelegationManager _delegationManager, IMetaSwap _metaSwap) Ownable(_owner) {
constructor(
address _owner,
address _swapApiSigner,
IDelegationManager _delegationManager,
IMetaSwap _metaSwap,
address _argsEqualityCheckEnforcer
)
Ownable(_owner)
{
swapApiSigner = _swapApiSigner;
delegationManager = _delegationManager;
metaSwap = _metaSwap;
argsEqualityCheckEnforcer = _argsEqualityCheckEnforcer;
emit SwapApiSignerUpdated(_swapApiSigner);
emit SetDelegationManager(_delegationManager);
emit SetMetaSwap(_metaSwap);
emit SetArgsEqualityCheckEnforcer(_argsEqualityCheckEnforcer);
}

////////////////////////////// External Methods //////////////////////////////
Expand All @@ -145,20 +202,34 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
receive() external payable { }

/**
* @notice Executes a token swap using a delegation and transfers the swapped tokens to the root delegator.
* @notice Executes a token swap using a delegation and transfers the swapped tokens to the root delegator, after validating
* signature and expiration.
* @dev The msg.sender must be the leaf delegator
* @param _apiData Encoded swap parameters, used by the aggregator.
* @param _signatureData Includes:
* - apiData Encoded swap parameters, used by the aggregator.
* - expiration Timestamp after which the signature is invalid.
* - signature Signature validating the provided apiData.
* @param _delegations Array of Delegation objects containing delegation-specific data, sorted leaf to root.
* @param _useTokenWhitelist Indicates whether the tokens must be validated or not.
*/
function swapByDelegation(bytes calldata _apiData, Delegation[] memory _delegations) external {
function swapByDelegation(
SignatureData calldata _signatureData,
Delegation[] memory _delegations,
bool _useTokenWhitelist
)
external
{
_validateSignature(_signatureData);

(string memory aggregatorId_, IERC20 tokenFrom_, IERC20 tokenTo_, uint256 amountFrom_, bytes memory swapData_) =
_decodeApiData(_apiData);
_decodeApiData(_signatureData.apiData);
uint256 delegationsLength_ = _delegations.length;

if (delegationsLength_ == 0) revert InvalidEmptyDelegations();
if (tokenFrom_ == tokenTo_) revert InvalidIdenticalTokens();
if (!isTokenAllowed[tokenFrom_]) revert TokenFromIsNotAllowed(tokenFrom_);
if (!isTokenAllowed[tokenTo_]) revert TokenToIsNotAllowed(tokenTo_);

_validateTokens(tokenFrom_, tokenTo_, _delegations, _useTokenWhitelist);

if (!isAggregatorAllowed[keccak256(abi.encode(aggregatorId_))]) revert AggregatorIdIsNotAllowed(aggregatorId_);
if (_delegations[0].delegator != msg.sender) revert NotLeafDelegator();

Expand Down Expand Up @@ -245,6 +316,15 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
_sendTokens(_tokenTo, obtainedAmount_, _recipient);
}

/**
* @notice Updates the address authorized to sign API requests.
* @param _newSigner The new authorized signer address.
*/
function setSwapApiSigner(address _newSigner) external onlyOwner {
swapApiSigner = _newSigner;
emit SwapApiSignerUpdated(_newSigner);
}

/**
* @notice Executes one calls on behalf of this contract,
* authorized by the DelegationManager.
Expand Down Expand Up @@ -350,6 +430,42 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
emit SentTokens(_token, _recipient, _amount);
}

/**
* @dev Validates that the tokens are whitelisted or not based on the _useTokenWhitelist flag.
* @dev Adds the argsCheckEnforcer args to later validate if the token whitelist must be have been used or not.
* @param _tokenFrom The input token of the swap.
* @param _tokenTo The output token of the swap.
* @param _delegations The delegation chain; the last delegation must include the ArgsEqualityCheckEnforcer.
* @param _useTokenWhitelist Flag indicating whether token whitelist checks should be enforced.
*/
function _validateTokens(
IERC20 _tokenFrom,
IERC20 _tokenTo,
Delegation[] memory _delegations,
bool _useTokenWhitelist
)
private
view
{
// The Args Enforcer must be the first caveat in the root delegation
uint256 lastIndex_ = _delegations.length - 1;
if (
_delegations[lastIndex_].caveats.length == 0
|| _delegations[lastIndex_].caveats[0].enforcer != argsEqualityCheckEnforcer
) {
revert MissingArgsEqualityCheckEnforcer();
}

// The args are set by this contract depending on the useTokenWhitelist flag
if (_useTokenWhitelist) {
if (!isTokenAllowed[_tokenFrom]) revert TokenFromIsNotAllowed(_tokenFrom);
if (!isTokenAllowed[_tokenTo]) revert TokenToIsNotAllowed(_tokenTo);
_delegations[lastIndex_].caveats[0].args = abi.encode(WHITELIST_ENFORCED);
} else {
_delegations[lastIndex_].caveats[0].args = abi.encode(WHITELIST_NOT_ENFORCED);
}
}

/**
* @dev Internal helper to decode aggregator data from `apiData`.
* @param _apiData Bytes that includes aggregatorId, tokenFrom, amountFrom, and the aggregator swap data.
Expand Down Expand Up @@ -402,4 +518,18 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {

return _token.balanceOf(address(this));
}

/**
* @dev Validates the expiration and signature of the provided apiData.
* @param _signatureData Contains the apiData, the expiration and signature.
*/
function _validateSignature(SignatureData memory _signatureData) private view {
if (block.timestamp > _signatureData.expiration) revert SignatureExpired();

bytes32 messageHash_ = keccak256(abi.encodePacked(_signatureData.apiData, _signatureData.expiration));
bytes32 ethSignedMessageHash_ = MessageHashUtils.toEthSignedMessageHash(messageHash_);

address recoveredSigner_ = ECDSA.recover(ethSignedMessageHash_, _signatureData.signature);
if (recoveredSigner_ != swapApiSigner) revert InvalidApiSignature();
}
}
Loading