-
Notifications
You must be signed in to change notification settings - Fork 25
/
ERC20TokenLimitModule.sol
144 lines (122 loc) · 5.85 KB
/
ERC20TokenLimitModule.sol
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
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;
import {UserOperationLib} from "@eth-infinitism/account-abstraction/core/UserOperationLib.sol";
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IExecutionHookModule} from "../../interfaces/IExecutionHookModule.sol";
import {Call, IModularAccount} from "../../interfaces/IModularAccount.sol";
import {IModule} from "../../interfaces/IModule.sol";
import {BaseModule, IERC165} from "../BaseModule.sol";
/// @title ERC20 Token Limit Module
/// @author ERC-6900 Authors
/// @notice This module supports an ERC20 token spend limit. This should be combined with a contract whitelist
/// module to make sure that token transfers not tracked by the module don't happen.
/// Note: this module is opinionated on what selectors can be called for token contracts to guard against weird
/// edge cases like DAI. You wouldn't be able to use uni v2 pairs directly as the pair contract is also the LP
/// token contract
contract ERC20TokenLimitModule is BaseModule, IExecutionHookModule {
using UserOperationLib for PackedUserOperation;
struct ERC20SpendLimit {
address token;
uint256 limit;
}
struct SpendLimit {
bool hasLimit;
uint256 limit;
}
mapping(uint32 entityId => mapping(address token => mapping(address account => SpendLimit))) public limits;
error ERC20NotAllowed(address);
error ExceededTokenLimit();
error InvalidCalldataLength();
error SelectorNotAllowed();
error SpendingRequestNotAllowed(bytes4);
/// @inheritdoc IExecutionHookModule
function preExecutionHook(uint32 entityId, address, uint256, bytes calldata data)
external
override
returns (bytes memory)
{
(bytes4 selector, bytes memory callData) = _getSelectorAndCalldata(data);
if (selector == IModularAccount.execute.selector) {
// when calling execute or ERC20 functions directly
(address token,, bytes memory innerCalldata) = abi.decode(callData, (address, uint256, bytes));
_decrementLimitIfApplies(entityId, token, innerCalldata);
} else if (selector == IModularAccount.executeBatch.selector) {
Call[] memory calls = abi.decode(callData, (Call[]));
for (uint256 i = 0; i < calls.length; i++) {
_decrementLimitIfApplies(entityId, calls[i].target, calls[i].data);
}
} else {
revert SpendingRequestNotAllowed(selector);
}
return "";
}
/// @inheritdoc IModule
/// @param data should be encoded with the entityId of the validation and a list of ERC20 spend limits
function onInstall(bytes calldata data) external override {
(uint32 entityId, ERC20SpendLimit[] memory spendLimits) = abi.decode(data, (uint32, ERC20SpendLimit[]));
for (uint8 i = 0; i < spendLimits.length; i++) {
address token = spendLimits[i].token;
updateLimits(entityId, token, true, spendLimits[i].limit);
}
}
/// @inheritdoc IModule
/// @notice uninstall this module can only clear limit for one token of one entity. To clear all limits, users
/// are recommended to use updateLimit for each token and entityId.
/// @param data should be encoded with the entityId of the validation and the token address to be uninstalled
function onUninstall(bytes calldata data) external override {
(address token, uint32 entityId) = abi.decode(data, (address, uint32));
delete limits[entityId][token][msg.sender];
}
/// @inheritdoc IExecutionHookModule
function postExecutionHook(uint32, bytes calldata) external pure override {
revert NotImplemented();
}
/// @inheritdoc IModule
function moduleId() external pure returns (string memory) {
return "erc6900.erc20-token-limit-module.1.0.0";
}
/// @notice Update the token limit of a validation
/// @param entityId The validation entityId to update
/// @param token The token address whose limit will be updated
/// @param newLimit The new limit of the token for the validation
function updateLimits(uint32 entityId, address token, bool hasLimit, uint256 newLimit) public {
if (token == address(0)) {
revert ERC20NotAllowed(address(0));
}
limits[entityId][token][msg.sender] = SpendLimit({hasLimit: hasLimit, limit: newLimit});
}
/// @inheritdoc BaseModule
function supportsInterface(bytes4 interfaceId) public view override(BaseModule, IERC165) returns (bool) {
return interfaceId == type(IExecutionHookModule).interfaceId || super.supportsInterface(interfaceId);
}
function _decrementLimitIfApplies(uint32 entityId, address token, bytes memory innerCalldata) internal {
SpendLimit storage spendLimit = limits[entityId][token][msg.sender];
if (!spendLimit.hasLimit) {
return;
}
if (innerCalldata.length < 68) {
revert InvalidCalldataLength();
}
bytes4 selector;
uint256 spend;
assembly ("memory-safe") {
selector := mload(add(innerCalldata, 32)) // 0:32 is arr len, 32:36 is selector
spend := mload(add(innerCalldata, 68)) // 36:68 is recipient, 68:100 is spend
}
if (_isAllowedERC20Function(selector)) {
uint256 limit = spendLimit.limit;
if (spend > limit) {
revert ExceededTokenLimit();
}
unchecked {
spendLimit.limit = limit - spend;
}
} else {
revert SelectorNotAllowed();
}
}
function _isAllowedERC20Function(bytes4 selector) internal pure returns (bool) {
return selector == IERC20.transfer.selector || selector == IERC20.approve.selector;
}
}