-
-
Notifications
You must be signed in to change notification settings - Fork 103
Expand file tree
/
Copy pathNativeTokenPeriodTransferEnforcer.sol
More file actions
237 lines (212 loc) · 11 KB
/
NativeTokenPeriodTransferEnforcer.sol
File metadata and controls
237 lines (212 loc) · 11 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
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity ^0.8.23;
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
import { CaveatEnforcer } from "./CaveatEnforcer.sol";
import { ModeCode } from "../utils/Types.sol";
/**
* @title NativeTokenPeriodTransferEnforcer
* @notice Enforces periodic transfer limits for native token (ETH) transfers.
* @dev This contract implements a mechanism by which a user may transfer up to a fixed amount of ETH (the period amount)
* during a given time period. The transferable amount resets at the beginning of each period and any unused ETH is
* forfeited once the period ends. Partial transfers within a period are allowed, but the total transfer in any period
* cannot exceed the specified limit.
* @dev This enforcer operates only in single execution call type and with default execution mode.
*/
contract NativeTokenPeriodTransferEnforcer is CaveatEnforcer {
using ExecutionLib for bytes;
////////////////////////////// State //////////////////////////////
struct PeriodicAllowance {
uint256 periodAmount; // Maximum transferable ETH (in wei) 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.
}
/**
* @dev Mapping from a delegation manager address and delegation hash to a PeriodicAllowance.
*/
mapping(address delegationManager => mapping(bytes32 delegationHash => PeriodicAllowance)) public periodicAllowances;
////////////////////////////// Events //////////////////////////////
/**
* @notice Emitted when a native token transfer is made, updating the transferred amount in the active period.
* @param sender The address initiating the transfer.
* @param redeemer The address that receives the ETH.
* @param delegationHash The hash identifying the delegation.
* @param periodAmount The maximum ETH (in wei) transferable per period.
* @param periodDuration The duration of each period (in seconds).
* @param startDate The timestamp when the first period begins.
* @param transferredInCurrentPeriod The total ETH (in wei) transferred in the current period after this transfer.
* @param transferTimestamp The block timestamp at which the transfer was executed.
*/
event TransferredInPeriod(
address indexed sender,
address indexed redeemer,
bytes32 indexed delegationHash,
uint256 periodAmount,
uint256 periodDuration,
uint256 startDate,
uint256 transferredInCurrentPeriod,
uint256 transferTimestamp
);
////////////////////////////// Public Methods //////////////////////////////
/**
* @notice Retrieves the available ETH by simulating the allowance if it has not been initialized.
* @param _delegationHash The hash identifying the delegation.
* @param _delegationManager The delegation manager address.
* @param _terms 96 packed bytes:
* - 32 bytes: periodAmount.
* - 32 bytes: periodDuration (in seconds).
* - 32 bytes: startDate for the first period.
* @return availableAmount_ The simulated available ETH (in wei) in the current period.
* @return isNewPeriod_ True if a new period would be in effect.
* @return currentPeriod_ The current period index as determined by the terms.
*/
function getAvailableAmount(
bytes32 _delegationHash,
address _delegationManager,
bytes calldata _terms
)
external
view
returns (uint256 availableAmount_, bool isNewPeriod_, uint256 currentPeriod_)
{
PeriodicAllowance memory storedAllowance_ = periodicAllowances[_delegationManager][_delegationHash];
if (storedAllowance_.startDate != 0) return _getAvailableAmount(storedAllowance_);
// Not yet initialized: simulate using provided terms.
(uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_) = getTermsInfo(_terms);
PeriodicAllowance memory allowance_ = PeriodicAllowance({
periodAmount: periodAmount_,
periodDuration: periodDuration_,
startDate: startDate_,
lastTransferPeriod: 0,
transferredInCurrentPeriod: 0
});
return _getAvailableAmount(allowance_);
}
/**
* @notice Hook called before a native ETH transfer to enforce the periodic transfer limit.
* @dev Reverts if the transfer value exceeds the available ETH for the current period.
* Expects `_terms` to be a 96-byte blob encoding: periodAmount, periodDuration, and startDate.
* @param _terms 96 packed bytes:
* - 32 bytes: periodAmount.
* - 32 bytes: periodDuration (in seconds).
* - 32 bytes: startDate for the first period.
* @param _mode The execution mode. (Must be Single callType, Default execType)
* @param _executionCallData The execution data encoded via ExecutionLib.encodeSingle.
* For native ETH transfers, decodeSingle returns (target, value, callData) and callData is expected to be empty.
* @param _delegationHash The hash identifying the delegation.
*/
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode _mode,
bytes calldata _executionCallData,
bytes32 _delegationHash,
address,
address _redeemer
)
public
override
onlySingleCallTypeMode(_mode)
onlyDefaultExecutionMode(_mode)
{
_validateAndConsumeTransfer(_terms, _executionCallData, _delegationHash, _redeemer);
}
/**
* @notice Decodes the native transfer terms.
* @dev Expects a 96-byte blob and extracts: periodAmount, periodDuration, and startDate.
* @param _terms 96 packed bytes:
* - 32 bytes: periodAmount.
* - 32 bytes: periodDuration (in seconds).
* - 32 bytes: startDate.
* @return periodAmount_ The maximum ETH (in wei) transferable per period.
* @return periodDuration_ The duration of each period in seconds.
* @return startDate_ The timestamp when the first period begins.
*/
function getTermsInfo(bytes calldata _terms)
public
pure
returns (uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_)
{
require(_terms.length == 96, "NativeTokenPeriodTransferEnforcer:invalid-terms-length");
periodAmount_ = uint256(bytes32(_terms[0:32]));
periodDuration_ = uint256(bytes32(_terms[32:64]));
startDate_ = uint256(bytes32(_terms[64:96]));
}
////////////////////////////// Internal Methods //////////////////////////////
/**
* @notice Validates and consumes a transfer by ensuring the transfer value does not exceed the available ETH.
* @dev Uses _getAvailableAmount to determine the available ETH and whether a new period has started.
* If a new period is detected, the transferred amount is reset before consuming the transfer.
* @param _terms The encoded transfer terms (periodAmount, periodDuration, startDate).
* @param _executionCallData The execution data (expected to be encoded via ExecutionLib.encodeSingle).
* For native transfers, decodeSingle returns (target, value, callData) and callData must be empty.
* @param _delegationHash The hash identifying the delegation.
*/
function _validateAndConsumeTransfer(
bytes calldata _terms,
bytes calldata _executionCallData,
bytes32 _delegationHash,
address _redeemer
)
private
{
(, uint256 value_,) = _executionCallData.decodeSingle();
(uint256 periodAmount_, uint256 periodDuration_, uint256 startDate_) = getTermsInfo(_terms);
PeriodicAllowance storage allowance_ = periodicAllowances[msg.sender][_delegationHash];
// Initialize the allowance on first use.
if (allowance_.startDate == 0) {
// Validate terms.
require(startDate_ > 0, "NativeTokenPeriodTransferEnforcer:invalid-zero-start-date");
require(periodAmount_ > 0, "NativeTokenPeriodTransferEnforcer:invalid-zero-period-amount");
require(periodDuration_ > 0, "NativeTokenPeriodTransferEnforcer:invalid-zero-period-duration");
// Ensure the transfer period has started.
require(block.timestamp >= startDate_, "NativeTokenPeriodTransferEnforcer:transfer-not-started");
allowance_.periodAmount = periodAmount_;
allowance_.periodDuration = periodDuration_;
allowance_.startDate = startDate_;
}
// Calculate available ETH using the current allowance state.
(uint256 available_, bool isNewPeriod_, uint256 currentPeriod_) = _getAvailableAmount(allowance_);
require(value_ <= available_, "NativeTokenPeriodTransferEnforcer:transfer-amount-exceeded");
// If a new period has started, update state before processing the transfer.
if (isNewPeriod_) {
allowance_.lastTransferPeriod = currentPeriod_;
allowance_.transferredInCurrentPeriod = 0;
}
allowance_.transferredInCurrentPeriod += value_;
emit TransferredInPeriod(
msg.sender,
_redeemer,
_delegationHash,
periodAmount_,
periodDuration_,
allowance_.startDate,
allowance_.transferredInCurrentPeriod,
block.timestamp
);
}
/**
* @notice Computes the available ETH that can be transferred in the current period.
* @dev Calculates the current period index based on `startDate` and `periodDuration`. Returns a tuple:
* - availableAmount_: Remaining ETH transferable in the current period.
* - isNewPeriod_: True if the last transfer period is not equal to the current period.
* - currentPeriod_: The current period index, with the first period starting at 1.
* If the current time is before the start date, availableAmount_ is 0.
* @param _allowance The PeriodicAllowance struct.
* @return availableAmount_ The ETH still available to transfer in the current period.
* @return isNewPeriod_ True if a new period has started since the last transfer.
* @return currentPeriod_ The current period index calculated from the start date.
*/
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 transferred = isNewPeriod_ ? 0 : _allowance.transferredInCurrentPeriod;
availableAmount_ = _allowance.periodAmount > transferred ? _allowance.periodAmount - transferred : 0;
}
}