@@ -3,14 +3,7 @@ pragma solidity ^0.8.20;
33
44import {UserOperationLib} from "@eth-infinitism/account-abstraction/core/UserOperationLib.sol " ;
55import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol " ;
6-
7- import {
8- AssociatedLinkedListSet,
9- AssociatedLinkedListSetLib,
10- SetValue
11- } from "@modular-account-libs/libraries/AssociatedLinkedListSetLib.sol " ;
126import {IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol " ;
13- import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol " ;
147
158import {IExecutionHookModule} from "../interfaces/IExecutionHookModule.sol " ;
169import {Call, IModularAccount} from "../interfaces/IModularAccount.sol " ;
@@ -27,29 +20,24 @@ import {BaseModule, IERC165} from "./BaseModule.sol";
2720/// token contract
2821contract ERC20TokenLimitModule is BaseModule , IExecutionHookModule {
2922 using UserOperationLib for PackedUserOperation;
30- using EnumerableSet for EnumerableSet.AddressSet;
31- using AssociatedLinkedListSetLib for AssociatedLinkedListSet;
3223
3324 struct ERC20SpendLimit {
3425 address token;
35- uint256 [] limits ;
26+ uint256 limit ;
3627 }
3728
38- string internal constant _NAME = "ERC20 Token Limit Module " ;
39- string internal constant _VERSION = "1.0.0 " ;
40- string internal constant _AUTHOR = "ERC-6900 Authors " ;
29+ struct SpendLimit {
30+ bool hasLimit;
31+ uint256 limit;
32+ }
4133
42- mapping (uint32 entityId = > mapping (address token = > mapping (address account = > uint256 limit ))) public limits;
43- AssociatedLinkedListSet internal _tokenList;
34+ mapping (uint32 entityId = > mapping (address token = > mapping (address account = > SpendLimit))) public limits;
4435
36+ error ERC20NotAllowed (address );
4537 error ExceededTokenLimit ();
46- error ExceededNumberOfEntities ();
38+ error InvalidCalldataLength ();
4739 error SelectorNotAllowed ();
48-
49- function updateLimits (uint32 entityId , address token , uint256 newLimit ) external {
50- _tokenList.tryAdd (msg .sender , SetValue.wrap (bytes30 (bytes20 (token))));
51- limits[entityId][token][msg .sender ] = newLimit;
52- }
40+ error SpendingRequestNotAllowed (bytes4 );
5341
5442 /// @inheritdoc IExecutionHookModule
5543 function preExecutionHook (uint32 entityId , address , uint256 , bytes calldata data )
@@ -60,54 +48,40 @@ contract ERC20TokenLimitModule is BaseModule, IExecutionHookModule {
6048 (bytes4 selector , bytes memory callData ) = _getSelectorAndCalldata (data);
6149
6250 if (selector == IModularAccount.execute.selector ) {
51+ // when calling execute or ERC20 functions directly
6352 (address token ,, bytes memory innerCalldata ) = abi.decode (callData, (address , uint256 , bytes ));
64- if (_tokenList.contains (msg .sender , SetValue.wrap (bytes30 (bytes20 (token))))) {
65- _decrementLimit (entityId, token, innerCalldata);
66- }
53+ _decrementLimitIfApplies (entityId, token, innerCalldata);
6754 } else if (selector == IModularAccount.executeBatch.selector ) {
6855 Call[] memory calls = abi.decode (callData, (Call[]));
6956 for (uint256 i = 0 ; i < calls.length ; i++ ) {
70- if (_tokenList.contains (msg .sender , SetValue.wrap (bytes30 (bytes20 (calls[i].target))))) {
71- _decrementLimit (entityId, calls[i].target, calls[i].data);
72- }
57+ _decrementLimitIfApplies (entityId, calls[i].target, calls[i].data);
7358 }
59+ } else {
60+ revert SpendingRequestNotAllowed (selector);
7461 }
75-
7662 return "" ;
7763 }
7864
7965 /// @inheritdoc IModule
66+ /// @param data should be encoded with the entityId of the validation and a list of ERC20 spend limits
8067 function onInstall (bytes calldata data ) external override {
81- (uint32 startEntityId , ERC20SpendLimit [] memory spendLimits ) =
82- abi.decode (data, (uint32 , ERC20SpendLimit []));
83-
84- if (startEntityId + spendLimits.length > type (uint32 ).max) {
85- revert ExceededNumberOfEntities ();
86- }
68+ (uint32 entityId , ERC20SpendLimit [] memory spendLimits ) = abi.decode (data, (uint32 , ERC20SpendLimit []));
8769
8870 for (uint8 i = 0 ; i < spendLimits.length ; i++ ) {
89- _tokenList.tryAdd (msg .sender , SetValue.wrap (bytes30 (bytes20 (spendLimits[i].token))));
90- for (uint256 j = 0 ; j < spendLimits[i].limits.length ; j++ ) {
91- limits[i + startEntityId][spendLimits[i].token][msg .sender ] = spendLimits[i].limits[j];
92- }
71+ address token = spendLimits[i].token;
72+ updateLimits (entityId, token, true , spendLimits[i].limit);
9373 }
9474 }
9575
9676 /// @inheritdoc IModule
77+ /// @notice uninstall this module can only clear limit for one token of one entity. To clear all limits, users
78+ /// are recommended to use updateLimit for each token and entityId.
79+ /// @param data should be encoded with the entityId of the validation and the token address to be uninstalled
9780 function onUninstall (bytes calldata data ) external override {
9881 (address token , uint32 entityId ) = abi.decode (data, (address , uint32 ));
9982 delete limits[entityId][token][msg .sender ];
10083 }
10184
102- function getTokensForAccount (address account ) external view returns (address [] memory tokens ) {
103- SetValue[] memory set = _tokenList.getAll (account);
104- tokens = new address [](set.length );
105- for (uint256 i = 0 ; i < tokens.length ; i++ ) {
106- tokens[i] = address (bytes20 (bytes32 (SetValue.unwrap (set[i]))));
107- }
108- return tokens;
109- }
110-
11185 /// @inheritdoc IExecutionHookModule
11286 function postExecutionHook (uint32 , bytes calldata ) external pure override {
11387 revert NotImplemented ();
@@ -118,27 +92,53 @@ contract ERC20TokenLimitModule is BaseModule, IExecutionHookModule {
11892 return "erc6900.erc20-token-limit-module.1.0.0 " ;
11993 }
12094
95+ /// @notice Update the token limit of a validation
96+ /// @param entityId The validation entityId to update
97+ /// @param token The token address whose limit will be updated
98+ /// @param newLimit The new limit of the token for the validation
99+ function updateLimits (uint32 entityId , address token , bool hasLimit , uint256 newLimit ) public {
100+ if (token == address (0 )) {
101+ revert ERC20NotAllowed (address (0 ));
102+ }
103+ limits[entityId][token][msg .sender ] = SpendLimit ({hasLimit: hasLimit, limit: newLimit});
104+ }
105+
121106 /// @inheritdoc BaseModule
122107 function supportsInterface (bytes4 interfaceId ) public view override (BaseModule, IERC165 ) returns (bool ) {
123108 return interfaceId == type (IExecutionHookModule).interfaceId || super .supportsInterface (interfaceId);
124109 }
125110
126- function _decrementLimit (uint32 entityId , address token , bytes memory innerCalldata ) internal {
111+ function _decrementLimitIfApplies (uint32 entityId , address token , bytes memory innerCalldata ) internal {
112+ SpendLimit storage spendLimit = limits[entityId][token][msg .sender ];
113+
114+ if (! spendLimit.hasLimit) {
115+ return ;
116+ }
117+
118+ if (innerCalldata.length < 68 ) {
119+ revert InvalidCalldataLength ();
120+ }
121+
127122 bytes4 selector;
128123 uint256 spend;
129- assembly {
124+ assembly ( "memory-safe" ) {
130125 selector := mload (add (innerCalldata, 32 )) // 0:32 is arr len, 32:36 is selector
131126 spend := mload (add (innerCalldata, 68 )) // 36:68 is recipient, 68:100 is spend
132127 }
133- if (selector == IERC20 .transfer. selector || selector == IERC20 .approve. selector ) {
134- uint256 limit = limits[entityId][token][ msg . sender ] ;
128+ if (_isAllowedERC20Function ( selector) ) {
129+ uint256 limit = spendLimit.limit ;
135130 if (spend > limit) {
136131 revert ExceededTokenLimit ();
137132 }
138- // solhint-disable-next-line reentrancy
139- limits[entityId][token][msg .sender ] = limit - spend;
133+ unchecked {
134+ spendLimit.limit = limit - spend;
135+ }
140136 } else {
141137 revert SelectorNotAllowed ();
142138 }
143139 }
140+
141+ function _isAllowedERC20Function (bytes4 selector ) internal pure returns (bool ) {
142+ return selector == IERC20 .transfer.selector || selector == IERC20 .approve.selector ;
143+ }
144144}
0 commit comments