11// SPDX-License-Identifier: MIT AND Apache-2.0
22pragma solidity 0.8.23 ;
33
4+ import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol " ;
5+ import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol " ;
46import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol " ;
57import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol " ;
68import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol " ;
@@ -15,22 +17,47 @@ import { CALLTYPE_SINGLE, EXECTYPE_DEFAULT } from "../utils/Constants.sol";
1517
1618/**
1719 * @title DelegationMetaSwapAdapter
18- * @notice Acts as a middleman to orchestrate token swaps using delegations
19- * and an aggregator (MetaSwap).
20+ * @notice Acts as a middleman to orchestrate token swaps using delegations and an aggregator (MetaSwap).
21+ * @dev This contract depends on an ArgsEqualityCheckEnforcer. The root delegation must include a caveat
22+ * with this enforcer as its first element. Its arguments indicate whether the swap should enforce the token
23+ * whitelist ("Token-Whitelist-Enforced") or not ("Token-Whitelist-Not-Enforced"). The root delegator is
24+ * responsible for including this enforcer to signal the desired behavior.
25+ *
26+ * @dev This adapter is intended to be used with the Swaps API. Accordingly, all API requests must include a valid
27+ * signature that incorporates an expiration timestamp. The signature is verified during swap execution to ensure
28+ * that it is still valid.
2029 */
2130contract DelegationMetaSwapAdapter is ExecutionHelper , Ownable2Step {
2231 using ModeLib for ModeCode;
2332 using ExecutionLib for bytes ;
2433 using SafeERC20 for IERC20 ;
2534
35+ struct SignatureData {
36+ bytes apiData;
37+ uint256 expiration;
38+ bytes signature;
39+ }
40+
2641 ////////////////////////////// State //////////////////////////////
2742
43+ /// @dev Constant value used to enforce the token whitelist
44+ string public constant WHITELIST_ENFORCED = "Token-Whitelist-Enforced " ;
45+
46+ /// @dev Constant value used to avoid enforcing the token whitelist
47+ string public constant WHITELIST_NOT_ENFORCED = "Token-Whitelist-Not-Enforced " ;
48+
2849 /// @dev The DelegationManager contract that has root access to this contract
2950 IDelegationManager public immutable delegationManager;
3051
3152 /// @dev The MetaSwap contract used to swap tokens
3253 IMetaSwap public immutable metaSwap;
3354
55+ /// @dev The enforcer used to compare args and terms
56+ address public immutable argsEqualityCheckEnforcer;
57+
58+ /// @dev Address of the API signer account.
59+ address public swapApiSigner;
60+
3461 /// @dev Indicates if a token is allowed to be used in the swaps
3562 mapping (IERC20 token = > bool allowed ) public isTokenAllowed;
3663
@@ -45,6 +72,9 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
4572 /// @dev Emitted when the MetaSwap contract address is set.
4673 event SetMetaSwap (IMetaSwap indexed newMetaSwap );
4774
75+ /// @dev Emitted when the Args Equality Check Enforcer contract address is set.
76+ event SetArgsEqualityCheckEnforcer (address indexed newArgsEqualityCheckEnforcer );
77+
4878 /// @dev Emitted when the contract sends tokens (or native tokens) to a recipient.
4979 event SentTokens (IERC20 indexed token , address indexed recipient , uint256 amount );
5080
@@ -54,6 +84,9 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
5484 /// @dev Emitted when the allowed aggregator ID status changes.
5585 event ChangedAggregatorIdStatus (bytes32 indexed aggregatorIdHash , string aggregatorId , bool status );
5686
87+ /// @dev Emitted when the Signer API is updated.
88+ event SwapApiSignerUpdated (address indexed newSigner );
89+
5790 ////////////////////////////// Errors //////////////////////////////
5891
5992 /// @dev Error thrown when the caller is not the delegation manager
@@ -62,7 +95,7 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
6295 /// @dev Error thrown when the call is not made by this contract itself.
6396 error NotSelf ();
6497
65- /// @dev Error thrown when msg.sender is not the leaf delegatior .
98+ /// @dev Error thrown when msg.sender is not the leaf delegator .
6699 error NotLeafDelegator ();
67100
68101 /// @dev Error thrown when an execution with an unsupported CallType is made.
@@ -87,7 +120,7 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
87120 error TokenToIsNotAllowed (IERC20 token );
88121
89122 /// @dev Error when the aggregator ID is not in the allow list.
90- error AggregatorIdIsNotAllowed (string );
123+ error AggregatorIdIsNotAllowed (string aggregatorId );
91124
92125 /// @dev Error when the input arrays of a function have different lengths.
93126 error InputLengthsMismatch ();
@@ -104,6 +137,15 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
104137 /// @dev Error when the amountFrom in the api data and swap data do not match.
105138 error AmountFromMismath ();
106139
140+ /// @dev Error when the delegations do not include the ArgsEqualityCheckEnforcer
141+ error MissingArgsEqualityCheckEnforcer ();
142+
143+ /// @dev Error thrown when API signature is invalid.
144+ error InvalidApiSignature ();
145+
146+ /// @dev Error thrown when the signature expiration has passed.
147+ error SignatureExpired ();
148+
107149 ////////////////////////////// Modifiers //////////////////////////////
108150
109151 /**
@@ -125,16 +167,31 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
125167 ////////////////////////////// Constructor //////////////////////////////
126168
127169 /**
128- * @notice Initializes the DelegationMetaSwapAdapter contract
129- * @param _owner The initial owner of the contract
130- * @param _delegationManager the address of the trusted DelegationManager contract that will have root access to this contract
131- * @param _metaSwap the address of the trusted MetaSwap contract.
170+ * @notice Initializes the DelegationMetaSwapAdapter contract.
171+ * @param _owner The initial owner of the contract.
172+ * @param _swapApiSigner The initial swap API signer.
173+ * @param _delegationManager The address of the trusted DelegationManager contract has privileged access to call
174+ * executeByExecutor based on a given delegation.
175+ * @param _metaSwap The address of the trusted MetaSwap contract.
176+ * @param _argsEqualityCheckEnforcer The address of the ArgsEqualityCheckEnforcer contract.
132177 */
133- constructor (address _owner , IDelegationManager _delegationManager , IMetaSwap _metaSwap ) Ownable (_owner) {
178+ constructor (
179+ address _owner ,
180+ address _swapApiSigner ,
181+ IDelegationManager _delegationManager ,
182+ IMetaSwap _metaSwap ,
183+ address _argsEqualityCheckEnforcer
184+ )
185+ Ownable (_owner)
186+ {
187+ swapApiSigner = _swapApiSigner;
134188 delegationManager = _delegationManager;
135189 metaSwap = _metaSwap;
190+ argsEqualityCheckEnforcer = _argsEqualityCheckEnforcer;
191+ emit SwapApiSignerUpdated (_swapApiSigner);
136192 emit SetDelegationManager (_delegationManager);
137193 emit SetMetaSwap (_metaSwap);
194+ emit SetArgsEqualityCheckEnforcer (_argsEqualityCheckEnforcer);
138195 }
139196
140197 ////////////////////////////// External Methods //////////////////////////////
@@ -145,20 +202,34 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
145202 receive () external payable { }
146203
147204 /**
148- * @notice Executes a token swap using a delegation and transfers the swapped tokens to the root delegator.
205+ * @notice Executes a token swap using a delegation and transfers the swapped tokens to the root delegator, after validating
206+ * signature and expiration.
149207 * @dev The msg.sender must be the leaf delegator
150- * @param _apiData Encoded swap parameters, used by the aggregator.
208+ * @param _signatureData Includes:
209+ * - apiData Encoded swap parameters, used by the aggregator.
210+ * - expiration Timestamp after which the signature is invalid.
211+ * - signature Signature validating the provided apiData.
151212 * @param _delegations Array of Delegation objects containing delegation-specific data, sorted leaf to root.
213+ * @param _useTokenWhitelist Indicates whether the tokens must be validated or not.
152214 */
153- function swapByDelegation (bytes calldata _apiData , Delegation[] memory _delegations ) external {
215+ function swapByDelegation (
216+ SignatureData calldata _signatureData ,
217+ Delegation[] memory _delegations ,
218+ bool _useTokenWhitelist
219+ )
220+ external
221+ {
222+ _validateSignature (_signatureData);
223+
154224 (string memory aggregatorId_ , IERC20 tokenFrom_ , IERC20 tokenTo_ , uint256 amountFrom_ , bytes memory swapData_ ) =
155- _decodeApiData (_apiData );
225+ _decodeApiData (_signatureData.apiData );
156226 uint256 delegationsLength_ = _delegations.length ;
157227
158228 if (delegationsLength_ == 0 ) revert InvalidEmptyDelegations ();
159229 if (tokenFrom_ == tokenTo_) revert InvalidIdenticalTokens ();
160- if (! isTokenAllowed[tokenFrom_]) revert TokenFromIsNotAllowed (tokenFrom_);
161- if (! isTokenAllowed[tokenTo_]) revert TokenToIsNotAllowed (tokenTo_);
230+
231+ _validateTokens (tokenFrom_, tokenTo_, _delegations, _useTokenWhitelist);
232+
162233 if (! isAggregatorAllowed[keccak256 (abi.encode (aggregatorId_))]) revert AggregatorIdIsNotAllowed (aggregatorId_);
163234 if (_delegations[0 ].delegator != msg .sender ) revert NotLeafDelegator ();
164235
@@ -245,6 +316,15 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
245316 _sendTokens (_tokenTo, obtainedAmount_, _recipient);
246317 }
247318
319+ /**
320+ * @notice Updates the address authorized to sign API requests.
321+ * @param _newSigner The new authorized signer address.
322+ */
323+ function setSwapApiSigner (address _newSigner ) external onlyOwner {
324+ swapApiSigner = _newSigner;
325+ emit SwapApiSignerUpdated (_newSigner);
326+ }
327+
248328 /**
249329 * @notice Executes one calls on behalf of this contract,
250330 * authorized by the DelegationManager.
@@ -350,6 +430,42 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
350430 emit SentTokens (_token, _recipient, _amount);
351431 }
352432
433+ /**
434+ * @dev Validates that the tokens are whitelisted or not based on the _useTokenWhitelist flag.
435+ * @dev Adds the argsCheckEnforcer args to later validate if the token whitelist must be have been used or not.
436+ * @param _tokenFrom The input token of the swap.
437+ * @param _tokenTo The output token of the swap.
438+ * @param _delegations The delegation chain; the last delegation must include the ArgsEqualityCheckEnforcer.
439+ * @param _useTokenWhitelist Flag indicating whether token whitelist checks should be enforced.
440+ */
441+ function _validateTokens (
442+ IERC20 _tokenFrom ,
443+ IERC20 _tokenTo ,
444+ Delegation[] memory _delegations ,
445+ bool _useTokenWhitelist
446+ )
447+ private
448+ view
449+ {
450+ // The Args Enforcer must be the first caveat in the root delegation
451+ uint256 lastIndex_ = _delegations.length - 1 ;
452+ if (
453+ _delegations[lastIndex_].caveats.length == 0
454+ || _delegations[lastIndex_].caveats[0 ].enforcer != argsEqualityCheckEnforcer
455+ ) {
456+ revert MissingArgsEqualityCheckEnforcer ();
457+ }
458+
459+ // The args are set by this contract depending on the useTokenWhitelist flag
460+ if (_useTokenWhitelist) {
461+ if (! isTokenAllowed[_tokenFrom]) revert TokenFromIsNotAllowed (_tokenFrom);
462+ if (! isTokenAllowed[_tokenTo]) revert TokenToIsNotAllowed (_tokenTo);
463+ _delegations[lastIndex_].caveats[0 ].args = abi.encode (WHITELIST_ENFORCED);
464+ } else {
465+ _delegations[lastIndex_].caveats[0 ].args = abi.encode (WHITELIST_NOT_ENFORCED);
466+ }
467+ }
468+
353469 /**
354470 * @dev Internal helper to decode aggregator data from `apiData`.
355471 * @param _apiData Bytes that includes aggregatorId, tokenFrom, amountFrom, and the aggregator swap data.
@@ -402,4 +518,18 @@ contract DelegationMetaSwapAdapter is ExecutionHelper, Ownable2Step {
402518
403519 return _token.balanceOf (address (this ));
404520 }
521+
522+ /**
523+ * @dev Validates the expiration and signature of the provided apiData.
524+ * @param _signatureData Contains the apiData, the expiration and signature.
525+ */
526+ function _validateSignature (SignatureData memory _signatureData ) private view {
527+ if (block .timestamp > _signatureData.expiration) revert SignatureExpired ();
528+
529+ bytes32 messageHash_ = keccak256 (abi.encodePacked (_signatureData.apiData, _signatureData.expiration));
530+ bytes32 ethSignedMessageHash_ = MessageHashUtils.toEthSignedMessageHash (messageHash_);
531+
532+ address recoveredSigner_ = ECDSA.recover (ethSignedMessageHash_, _signatureData.signature);
533+ if (recoveredSigner_ != swapApiSigner) revert InvalidApiSignature ();
534+ }
405535}
0 commit comments