-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathIntervalSpendingLimitPolicy.sol
More file actions
254 lines (226 loc) · 8.27 KB
/
IntervalSpendingLimitPolicy.sol
File metadata and controls
254 lines (226 loc) · 8.27 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
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
import { ConfigId } from "smart-sessions/DataTypes.sol";
import {
IActionPolicy,
IPolicy,
VALIDATION_SUCCESS,
VALIDATION_FAILED
} from "smart-sessions/interfaces/IPolicy.sol";
import { IERC20 } from "forge-std/interfaces/IERC20.sol";
import { IERC165 } from "forge-std/interfaces/IERC165.sol";
import { EnumerableSet } from "smart-sessions/utils/EnumerableSet4337.sol";
import { DateTimeLib } from "solady/utils/DateTimeLib.sol";
address constant NATIVE_TOKEN = address(type(uint160).max);
enum Intervals {
Daily,
Weekly,
Monthly
}
/**
* This contract is a fork of ERC20SpendingLimitPolicy.sol from erc7579/smartsessions.
* The difference is the inclusion of added logic to reset the accrued spend after
* a defined interval and include native token transfers.
*
* Note: This policy relies on the TIMESTAMP opcode during validation which is not
* compliant with the canonical mempool. This is required to ensure time intervals
* work as expected.
*/
contract IntervalSpendingLimitPolicy is IActionPolicy {
using EnumerableSet for EnumerableSet.AddressSet;
event TokenSpent(
ConfigId id,
address multiplexer,
address token,
address account,
uint256 amount,
uint256 remaining
);
event IntervalUpdated(
ConfigId id,
address multiplexer,
address token,
address account,
uint256 previous,
uint256 current
);
error InvalidTokenAddress(address token);
error InvalidLimit(uint256 limit);
struct TokenPolicyData {
uint256 alreadySpent;
uint256 spendingLimit;
uint256 currentIntervalEnd;
Intervals interval;
}
mapping(ConfigId id => mapping(address multiplexer => EnumerableSet.AddressSet tokensEnabled))
internal $tokens;
mapping(
ConfigId id
=> mapping(
address mulitplexer
=> mapping(address token => mapping(address userOpSender => TokenPolicyData))
)
) internal $policyData;
function _getPolicy(
ConfigId id,
address userOpSender,
address token
)
internal
view
returns (TokenPolicyData storage s)
{
if (token == address(0)) revert InvalidTokenAddress(token);
s = $policyData[id][msg.sender][token][userOpSender];
}
function supportsInterface(bytes4 interfaceID) external pure override returns (bool) {
return (
interfaceID == type(IERC165).interfaceId || interfaceID == type(IPolicy).interfaceId
|| interfaceID == type(IActionPolicy).interfaceId
);
}
function initializeWithMultiplexer(
address account,
ConfigId configId,
bytes calldata initData
)
external
{
(Intervals interval, address[] memory tokens, uint256[] memory limits) =
abi.decode(initData, (Intervals, address[], uint256[]));
EnumerableSet.AddressSet storage $t = $tokens[configId][msg.sender];
uint256 length_i = $t.length(account);
if (length_i > 0) {
for (uint256 i; i < length_i; i++) {
address token = $t.at(account, i);
TokenPolicyData storage $ =
_getPolicy({ id: configId, userOpSender: account, token: token });
$.spendingLimit = 0;
$.alreadySpent = 0;
}
$t.removeAll(account);
}
for (uint256 i; i < tokens.length; i++) {
address token = tokens[i];
uint256 limit = limits[i];
if (token == address(0)) revert InvalidTokenAddress(token);
if (limit == 0) revert InvalidLimit(limit);
TokenPolicyData storage $ =
_getPolicy({ id: configId, userOpSender: account, token: token });
$.spendingLimit = limit;
$.currentIntervalEnd = _getNextIntervalTimestamp(interval);
$.interval = interval;
$t.add(account, token);
}
emit IPolicy.PolicySet(configId, msg.sender, account);
}
function _isTokenTransfer(
address target,
uint256 value,
bytes calldata callData
)
internal
pure
returns (bool isTransfer, address token, uint256 amount)
{
// Detect a native ETH transfer. We don't check that value is more than
// 0 since we assume a 0 ETH transfer is a valid use case.
if (callData.length == 0) return (true, NATIVE_TOKEN, value);
// Assuming callData is not nil, then value has to be 0.
if (value != 0) return (false, address(0), 0);
bytes4 functionSelector = bytes4(callData[0:4]);
if (functionSelector == IERC20.approve.selector) {
(, amount) = abi.decode(callData[4:], (address, uint256));
return (true, target, amount);
} else if (functionSelector == IERC20.transfer.selector) {
(, amount) = abi.decode(callData[4:], (address, uint256));
return (true, target, amount);
} else if (functionSelector == IERC20.transferFrom.selector) {
(,, amount) = abi.decode(callData[4:], (address, address, uint256));
return (true, target, amount);
}
return (false, address(0), 0);
}
function _getNextIntervalTimestamp(Intervals interval)
internal
view
returns (uint256 timestamp)
{
// This is the line that will violate the TIMESTAMP restriction for the
// canonical UserOperation mempool.
uint256 currentTimestamp = block.timestamp;
if (interval == Intervals.Daily) {
(uint256 currentYear, uint256 currentMonth, uint256 currentDay) =
DateTimeLib.timestampToDate(currentTimestamp);
timestamp = DateTimeLib.addDays(
DateTimeLib.dateToTimestamp(currentYear, currentMonth, currentDay), 1
);
} else if (interval == Intervals.Weekly) {
timestamp = DateTimeLib.addDays(DateTimeLib.mondayTimestamp(currentTimestamp), 7);
} else if (interval == Intervals.Monthly) {
(uint256 currentYear, uint256 currentMonth,) =
DateTimeLib.timestampToDate(currentTimestamp);
timestamp =
DateTimeLib.addMonths(DateTimeLib.dateToTimestamp(currentYear, currentMonth, 1), 1);
}
}
function _getSpentAmountForInterval(
TokenPolicyData storage $,
ConfigId id,
address target,
address account
)
internal
returns (uint256 alreadySpent)
{
uint256 nextIntervalEnd = _getNextIntervalTimestamp($.interval);
uint256 currentIntervalEnd = $.currentIntervalEnd;
alreadySpent = $.alreadySpent;
if (nextIntervalEnd > currentIntervalEnd) {
alreadySpent = 0;
$.currentIntervalEnd = nextIntervalEnd;
emit IntervalUpdated(
id, msg.sender, target, account, currentIntervalEnd, nextIntervalEnd
);
}
}
function _check(
TokenPolicyData storage $,
ConfigId id,
address token,
address account,
uint256 amount,
uint256 alreadySpent
)
internal
returns (uint256)
{
uint256 spendingLimit = $.spendingLimit;
uint256 newAmount = alreadySpent + amount;
if (newAmount > spendingLimit) {
return VALIDATION_FAILED;
} else {
$.alreadySpent = newAmount;
emit TokenSpent(id, msg.sender, token, account, amount, spendingLimit - newAmount);
return VALIDATION_SUCCESS;
}
}
function checkAction(
ConfigId id,
address account,
address target,
uint256 value,
bytes calldata callData
)
external
override
returns (uint256)
{
(bool isTokenTransfer, address token, uint256 amount) =
_isTokenTransfer(target, value, callData);
if (!isTokenTransfer) return VALIDATION_FAILED;
TokenPolicyData storage $ = _getPolicy({ id: id, userOpSender: account, token: token });
uint256 alreadySpent = _getSpentAmountForInterval($, id, token, account);
return _check($, id, token, account, amount, alreadySpent);
}
}