Skip to content

Commit 0f8e128

Browse files
authored
MetaSwap Adapter Optional Token Whitelist And Added Swaps ApiData Signature Validation (#72)
* Optional token whitelist validation * Added whitelist flag to redeemByDelegation * chore: added deployment script * fix: issue in the deployment script * chore: improved enforce whitelist to const values * Added Swaps ApiData Signature Validation (#86)
1 parent 0205e0d commit 0f8e128

5 files changed

Lines changed: 462 additions & 51 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032
66
MULTISIG_DELEGATOR_IMPLEMENTATION_ADDRESS=
77
META_SWAP_ADAPTER_OWNER_ADDRESS=
88
METASWAP_ADDRESS=
9+
SWAPS_API_SIGNER_ADDRESS=
10+
ARGS_EQUALITY_CHECK_ENFORCER_ADDRESS=
911

1012
# Required for verifying contracts
1113
ETHERSCAN_API_KEY=

script/DeployCaveatEnforcers.s.sol

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { ERC20PeriodTransferEnforcer } from "../src/enforcers/ERC20PeriodTransfe
1919
import { ERC721BalanceGteEnforcer } from "../src/enforcers/ERC721BalanceGteEnforcer.sol";
2020
import { ERC721TransferEnforcer } from "../src/enforcers/ERC721TransferEnforcer.sol";
2121
import { ERC1155BalanceGteEnforcer } from "../src/enforcers/ERC1155BalanceGteEnforcer.sol";
22-
import { ExactCalldataBatchEnforcer } from "../src/enforcers/ExactCalldataBatchEnforcer.sol";
2322
import { ExactCalldataEnforcer } from "../src/enforcers/ExactCalldataEnforcer.sol";
2423
import { ExactExecutionBatchEnforcer } from "../src/enforcers/ExactExecutionBatchEnforcer.sol";
2524
import { ExactExecutionEnforcer } from "../src/enforcers/ExactExecutionEnforcer.sol";
@@ -104,9 +103,6 @@ contract DeployCaveatEnforcers is Script {
104103
deployedAddress = address(new ERC1155BalanceGteEnforcer{ salt: salt }());
105104
console2.log("ERC1155BalanceGteEnforcer: %s", deployedAddress);
106105

107-
deployedAddress = address(new ExactCalldataBatchEnforcer{ salt: salt }());
108-
console2.log("ExactCalldataBatchEnforcer: %s", deployedAddress);
109-
110106
deployedAddress = address(new ExactCalldataEnforcer{ salt: salt }());
111107
console2.log("ExactCalldataEnforcer: %s", deployedAddress);
112108

script/DeployDelegationMetaSwapAdapter.s.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ contract DeployDelegationMetaSwapAdapter is Script {
2020
bytes32 salt;
2121
address deployer;
2222
address metaSwapAdapterOwner;
23+
address swapApiSignerEnforcer;
2324
IDelegationManager delegationManager;
2425
IMetaSwap metaSwap;
26+
address argsEqualityCheckEnforcer;
2527

2628
function setUp() public {
2729
salt = bytes32(abi.encodePacked(vm.envString("SALT")));
2830
metaSwapAdapterOwner = vm.envAddress("META_SWAP_ADAPTER_OWNER_ADDRESS");
2931
delegationManager = IDelegationManager(vm.envAddress("DELEGATION_MANAGER_ADDRESS"));
3032
metaSwap = IMetaSwap(vm.envAddress("METASWAP_ADDRESS"));
33+
swapApiSignerEnforcer = vm.envAddress("SWAPS_API_SIGNER_ADDRESS");
34+
argsEqualityCheckEnforcer = vm.envAddress("ARGS_EQUALITY_CHECK_ENFORCER_ADDRESS");
3135
deployer = msg.sender;
3236
console2.log("~~~");
3337
console2.log("Deployer: %s", address(deployer));
@@ -40,8 +44,11 @@ contract DeployDelegationMetaSwapAdapter is Script {
4044
console2.log("~~~");
4145
vm.startBroadcast();
4246

43-
address delegationMetaSwapAdapter =
44-
address(new DelegationMetaSwapAdapter{ salt: salt }(metaSwapAdapterOwner, delegationManager, metaSwap));
47+
address delegationMetaSwapAdapter = address(
48+
new DelegationMetaSwapAdapter{ salt: salt }(
49+
metaSwapAdapterOwner, swapApiSignerEnforcer, delegationManager, metaSwap, argsEqualityCheckEnforcer
50+
)
51+
);
4552
console2.log("DelegationMetaSwapAdapter: %s", delegationMetaSwapAdapter);
4653

4754
vm.stopBroadcast();

src/helpers/DelegationMetaSwapAdapter.sol

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: MIT AND Apache-2.0
22
pragma 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";
46
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
57
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
68
import { 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
*/
2130
contract 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

Comments
 (0)