-
-
Notifications
You must be signed in to change notification settings - Fork 102
Expand file tree
/
Copy pathMultiTokenPeriodEnforcer.sol
More file actions
397 lines (362 loc) · 17.8 KB
/
MultiTokenPeriodEnforcer.sol
File metadata and controls
397 lines (362 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity ^0.8.23;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
import { CaveatEnforcer } from "./CaveatEnforcer.sol";
import { ModeCode } from "../utils/Types.sol";
/**
* @title MultiTokenPeriodEnforcer
* @notice Enforces periodic transfer limits for multiple tokens per delegation.
* @dev The enforcer expects the _terms to be a concatenation of one or more 116-byte configurations.
* Each 116-byte segment encodes:
* - 20 bytes: token address (address(0) indicates a native transfer)
* - 32 bytes: periodAmount.
* - 32 bytes: periodDuration (in seconds).
* - 32 bytes: startDate for the first period.
*
* Duplicate token entries in the terms are supported. Each configuration is identified
* by its index, allowing the same token to have multiple distinct allowances.
* The internal mapping includes the index in its hash key, enabling separate
* tracking per configuration.
*
* Additionally, the enforcer does not support restrictions on the recipient address or
* arbitrary calldata. For ERC20 transfers, the execution data is strictly required to
* match the IERC20.transfer function selector with a zero ETH value, and for native transfers,
* only an empty calldata is permitted.
*
* The _executionCallData always contains instructions for one token transfer.
*/
contract MultiTokenPeriodEnforcer is CaveatEnforcer {
using ExecutionLib for bytes;
////////////////////////////// State //////////////////////////////
struct PeriodicAllowance {
uint256 periodAmount; // Maximum transferable amount per period.
uint256 periodDuration; // Duration of each period in seconds.
uint256 startDate; // Timestamp when the first period begins.
uint256 lastTransferPeriod; // The period index in which the last transfer was made.
uint256 transferredInCurrentPeriod; // Cumulative amount transferred in the current period.
}
// Mapping from hash key => PeriodicAllowance
mapping(bytes32 hashKey => PeriodicAllowance) public periodicAllowances;
////////////////////////////// Events //////////////////////////////
/**
* @notice Emitted when a transfer is made and the allowance is updated.
* @param sender The address initiating the transfer.
* @param redeemer The address receiving the tokens/ETH.
* @param delegationHash The hash identifying the delegation.
* @param token The token contract address; for native transfers this is address(0).
* @param periodAmount The maximum transferable amount per period.
* @param periodDuration The duration of each period in seconds.
* @param startDate The timestamp when the first period begins.
* @param transferredInCurrentPeriod The total transferred in the current period after this transfer.
* @param transferTimestamp The block timestamp when the transfer occurred.
*/
event TransferredInPeriod(
address indexed sender,
address indexed redeemer,
bytes32 indexed delegationHash,
address token,
uint256 periodAmount,
uint256 periodDuration,
uint256 startDate,
uint256 transferredInCurrentPeriod,
uint256 transferTimestamp
);
////////////////////////////// External Methods //////////////////////////////
/**
* @notice Generates the key that identifies the run. Produced by the hash of the values used.
* @param _caller Address of the sender calling the enforcer.
* @param _token Token being compared in the beforeHook and afterHook.
* @param _delegationHash The hash of the delegation.
* @param _index The token configuration index.
* @return The hash to be used as key of the mapping.
*/
function getHashKey(address _caller, address _token, bytes32 _delegationHash, uint256 _index) external pure returns (bytes32) {
return _getHashKey(_caller, _token, _delegationHash, _index);
}
////////////////////////////// Public Methods //////////////////////////////
/**
* @notice Retrieves the available amount along with period details for a specific token.
* @param _delegationHash The delegation hash.
* @param _delegationManager The delegation manager's address.
* @param _terms A concatenation of one or more 116-byte configurations.
* @param _args A single uint256 value representing the index of the token configuration to use.
* @return availableAmount_ The remaining transferable amount in the current period.
* @return isNewPeriod_ True if a new period has begun.
* @return currentPeriod_ The current period index.
*/
function getAvailableAmount(
bytes32 _delegationHash,
address _delegationManager,
bytes calldata _terms,
bytes calldata _args
)
external
view
returns (uint256 availableAmount_, bool isNewPeriod_, uint256 currentPeriod_)
{
(bytes32 hashKey_, uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_) =
_getValues(_delegationHash, _delegationManager, _terms, _args);
PeriodicAllowance memory storedAllowance_ = periodicAllowances[hashKey_];
if (storedAllowance_.startDate != 0) {
return _getAvailableAmount(storedAllowance_);
}
// Not yet initialized; simulate using provided terms.
PeriodicAllowance memory allowance_ = PeriodicAllowance({
periodAmount: periodAmount_,
periodDuration: periodDuration_,
startDate: startDate_,
lastTransferPeriod: 0,
transferredInCurrentPeriod: 0
});
return _getAvailableAmount(allowance_);
}
/**
* @notice Decodes all configurations contained in _terms.
* @dev Expects _terms length to be a multiple of 116.
* @param _terms A concatenation of 116-byte configurations.
* @return tokens_ An array of token addresses.
* @return periodAmounts_ An array of period amounts.
* @return periodDurations_ An array of period durations (in seconds).
* @return startDates_ An array of start dates for the first period.
*/
function getAllTermsInfo(bytes calldata _terms)
external
pure
returns (
address[] memory tokens_,
uint256[] memory periodAmounts_,
uint256[] memory periodDurations_,
uint256[] memory startDates_
)
{
uint256 termsLength_ = _terms.length;
require(termsLength_ % 116 == 0 && termsLength_ != 0, "MultiTokenPeriodEnforcer:invalid-terms-length");
uint256 numConfigs_ = termsLength_ / 116;
tokens_ = new address[](numConfigs_);
periodAmounts_ = new uint256[](numConfigs_);
periodDurations_ = new uint256[](numConfigs_);
startDates_ = new uint256[](numConfigs_);
// Loop over each configuration using its index.
for (uint256 i = 0; i < numConfigs_; ++i) {
// Calculate the starting offset for this configuration.
uint256 offset_ = i * 116;
// Get the token address from the first 20 bytes.
tokens_[i] = address(bytes20(_terms[offset_:offset_ + 20]));
// Get the periodAmount from the next 32 bytes.
periodAmounts_[i] = uint256(bytes32(_terms[offset_ + 20:offset_ + 52]));
// Get the periodDuration from the following 32 bytes.
periodDurations_[i] = uint256(bytes32(_terms[offset_ + 52:offset_ + 84]));
// Get the startDate from the final 32 bytes.
startDates_[i] = uint256(bytes32(_terms[offset_ + 84:offset_ + 116]));
}
}
/**
* @notice Hook called before a transfer to enforce the periodic limit.
* @dev For ERC20 transfers, expects _executionCallData to decode to (target,, callData)
* with callData length of 68, beginning with IERC20.transfer.selector and zero value.
* For native transfers, expects _executionCallData to decode to (target, value, callData)
* with an empty callData.
* @param _terms A concatenation of one or more 116-byte configurations.
* @param _args A single uint256 value representing the index of the token configuration to use.
* @param _mode The execution mode (must be single callType, default execType).
* @param _executionCallData The encoded execution data.
* @param _delegationHash The delegation hash.
* @param _redeemer The address intended to receive the tokens/ETH.
*/
function beforeHook(
bytes calldata _terms,
bytes calldata _args,
ModeCode _mode,
bytes calldata _executionCallData,
bytes32 _delegationHash,
address,
address _redeemer
)
public
override
onlySingleCallTypeMode(_mode)
onlyDefaultExecutionMode(_mode)
{
_validateAndConsumeTransfer(_terms, _args, _executionCallData, _delegationHash, _redeemer);
}
/**
* @notice Retrieves the configuration for a specific token index from _terms.
* @dev Expects _terms length to be a multiple of 116.
* @param _terms A concatenation of 116-byte configurations.
* @param _tokenIndex The index of the token configuration to retrieve.
* @return token_ The token address at the specified index.
* @return periodAmount_ The maximum transferable amount for this token.
* @return periodDuration_ The period duration (in seconds) for this token.
* @return startDate_ The start date for the first period.
*/
function getTermsInfo(
bytes calldata _terms,
uint256 _tokenIndex
)
public
pure
returns (address token_, uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_)
{
uint256 termsLength_ = _terms.length;
require(termsLength_ != 0 && termsLength_ % 116 == 0, "MultiTokenPeriodEnforcer:invalid-terms-length");
uint256 numConfigs_ = termsLength_ / 116;
require(_tokenIndex < numConfigs_, "MultiTokenPeriodEnforcer:invalid-token-index");
uint256 offset_ = _tokenIndex * 116;
token_ = address(bytes20(_terms[offset_:offset_ + 20]));
periodAmount_ = uint256(bytes32(_terms[offset_ + 20:offset_ + 52]));
periodDuration_ = uint256(bytes32(_terms[offset_ + 52:offset_ + 84]));
startDate_ = uint256(bytes32(_terms[offset_ + 84:offset_ + 116]));
}
////////////////////////////// Internal Methods //////////////////////////////
/**
* @notice Validates and consumes a transfer (native or ERC20) by ensuring the amount does not exceed the available limit.
* @dev Decodes the execution data based on token type:
* - For native transfers (_token == address(0)): expect no calldata, value greater than zero.
* - For ERC20 transfers (_token != address(0)): requires callData length to be 68 with a
* valid IERC20.transfer selector, and zero value.
* @param _terms The concatenated configurations.
* @param _args A single uint256 value representing the index of the token configuration to use.
* @param _executionCallData The encoded execution data.
* @param _delegationHash The delegation hash.
* @param _redeemer The address intended to receive the tokens/ETH.
*/
function _validateAndConsumeTransfer(
bytes calldata _terms,
bytes calldata _args,
bytes calldata _executionCallData,
bytes32 _delegationHash,
address _redeemer
)
private
{
uint256 transferAmount_;
address token_;
{
// Decode _executionCallData using decodeSingle.
(address target_, uint256 value_, bytes calldata callData_) = _executionCallData.decodeSingle();
if (callData_.length == 68) {
// ERC20 transfer.
require(value_ == 0, "MultiTokenPeriodEnforcer:invalid-value-in-erc20-transfer");
require(bytes4(callData_[0:4]) == IERC20.transfer.selector, "MultiTokenPeriodEnforcer:invalid-method");
token_ = target_;
transferAmount_ = uint256(bytes32(callData_[36:68]));
} else if (callData_.length == 0) {
// Native transfer.
require(value_ > 0, "MultiTokenPeriodEnforcer:invalid-zero-value-in-native-transfer");
token_ = address(0);
transferAmount_ = value_;
} else {
// If callData length is neither 68 nor 0, revert.
revert("MultiTokenPeriodEnforcer:invalid-call-data-length");
}
}
uint256 index_ = abi.decode(_args, (uint256));
// Get the token configuration from the specified index
(address configuredToken_, uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_) =
getTermsInfo(_terms, index_);
// Verify that the token in the execution matches the configured token
require(token_ == configuredToken_, "MultiTokenPeriodEnforcer:token-mismatch");
// Use the hash key for the mapping
bytes32 hashKey_ = _getHashKey(msg.sender, token_, _delegationHash, index_);
PeriodicAllowance storage allowance_ = periodicAllowances[hashKey_];
// Initialize the allowance if not already set.
if (allowance_.startDate == 0) {
require(startDate_ > 0, "MultiTokenPeriodEnforcer:invalid-zero-start-date");
require(periodAmount_ > 0, "MultiTokenPeriodEnforcer:invalid-zero-period-amount");
require(periodDuration_ > 0, "MultiTokenPeriodEnforcer:invalid-zero-period-duration");
require(block.timestamp >= startDate_, "MultiTokenPeriodEnforcer:transfer-not-started");
allowance_.periodAmount = periodAmount_;
allowance_.periodDuration = periodDuration_;
allowance_.startDate = startDate_;
}
// Determine the available amount.
(uint256 availableAmount_, bool isNewPeriod_, uint256 currentPeriod_) = _getAvailableAmount(allowance_);
require(transferAmount_ <= availableAmount_, "MultiTokenPeriodEnforcer:transfer-amount-exceeded");
// Reset transferred amount if a new period has begun.
if (isNewPeriod_) {
allowance_.lastTransferPeriod = currentPeriod_;
allowance_.transferredInCurrentPeriod = 0;
}
allowance_.transferredInCurrentPeriod += transferAmount_;
emit TransferredInPeriod(
msg.sender,
_redeemer,
_delegationHash,
token_,
periodAmount_,
periodDuration_,
startDate_,
allowance_.transferredInCurrentPeriod,
block.timestamp
);
}
/**
* @notice Computes the available amount for the current period.
* @dev If block.timestamp is before startDate, available amount is 0.
* @param _allowance The PeriodicAllowance struct.
* @return availableAmount_ The remaining transferable amount in the current period.
* @return isNewPeriod_ True if the last transfer period is not equal to the current period.
* @return currentPeriod_ The current period index.
*/
function _getAvailableAmount(PeriodicAllowance memory _allowance)
internal
view
returns (uint256 availableAmount_, bool isNewPeriod_, uint256 currentPeriod_)
{
if (block.timestamp < _allowance.startDate) {
return (0, false, 0);
}
currentPeriod_ = (block.timestamp - _allowance.startDate) / _allowance.periodDuration + 1;
isNewPeriod_ = (_allowance.lastTransferPeriod != currentPeriod_);
uint256 alreadyTransferred_ = isNewPeriod_ ? 0 : _allowance.transferredInCurrentPeriod;
availableAmount_ = _allowance.periodAmount > alreadyTransferred_ ? _allowance.periodAmount - alreadyTransferred_ : 0;
}
/**
* @notice Generates the key that identifies the run. Produced by the hash of the values used.
* @param _delegationManager The delegation manager's address.
* @param _token The token address.
* @param _delegationHash The hash of the delegation.
* @param _index The token configuration index.
* @return The hash to be used as key of the mapping.
*/
function _getHashKey(
address _delegationManager,
address _token,
bytes32 _delegationHash,
uint256 _index
)
private
pure
returns (bytes32)
{
return keccak256(abi.encode(_delegationManager, _token, _delegationHash, _index));
}
/**
* @notice Extracts and processes values from delegation terms and arguments
* @dev Decodes the index from args, gets token info from terms, and generates a unique hash key
* @param _delegationHash The hash of the delegation
* @param _delegationManager The address of the delegation manager contract
* @param _terms The encoded terms containing token configurations
* @param _args The encoded arguments containing the token configuration index
* @return hashKey_ A unique hash key for identifying this specific delegation/token combination
* @return periodAmount_ The maximum amount that can be transferred per period
* @return periodDuration_ The duration of each period in seconds
* @return startDate_ The timestamp when the periodic allowance begins
*/
function _getValues(
bytes32 _delegationHash,
address _delegationManager,
bytes calldata _terms,
bytes calldata _args
)
internal
pure
returns (bytes32 hashKey_, uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_)
{
uint256 index_ = abi.decode(_args, (uint256));
address token_;
(token_, periodAmount_, periodDuration_, startDate_) = getTermsInfo(_terms, index_);
hashKey_ = _getHashKey(_delegationManager, token_, _delegationHash, index_);
}
}