forked from MetaMask/delegation-framework
-
Notifications
You must be signed in to change notification settings - Fork 0
/
.cursorrules
326 lines (257 loc) · 14.3 KB
/
.cursorrules
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
# Developing Smart Contracts for Delegation Systems
This guide focuses on creating smart contracts that work seamlessly with the MetaMask Delegation Toolkit. The key principle is to keep your contracts simple, focused on core functionality, and completely unaware of the delegation system itself.
## Core Principles
1. **Simplicity**: Contracts should focus solely on their core business logic.
2. **Owner-centric**: Use `onlyOwner` modifiers for privileged functions.
3. **Delegation-agnostic**: Contracts should not reference Delegation, DelegationManager, or mode encoding.
4. **Extensibility**: Design core functions to be easily extended through the delegation framework.
## Contract Structure
Here's an example of a basic contract structure:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is ERC721, Ownable {
constructor(string memory name, string memory symbol) ERC721(name, symbol) Ownable(msg.sender) {}
function mint(address to, uint256 tokenId) public onlyOwner {
_mint(to, tokenId);
}
}
```
## Core Functions
### Minting
The `mint` function is a simple example of a core function that can be easily extended through the delegation framework.
## Using Caveat Enforcers
Caveat enforcers allow you to add specific conditions or restrictions to delegations. The MetaMask Delegation Toolkit provides several out-of-the-box caveat enforcers:
- `AllowedCalldataEnforcer.sol`
- `AllowedMethodsEnforcer.sol`
- `AllowedTargetsEnforcer.sol`
- `BlockNumberEnforcer.sol`
- `DeployedEnforcer.sol`
- `ERC20TransferAmountEnforcer.sol`
- `ERC20BalanceGteEnforcer.sol`
- `NonceEnforcer.sol`
- `LimitedCallsEnforcer.sol`
- `IdEnforcer.sol`
- `TimestampEnforcer.sol`
- `ValueLteEnforcer.sol`
So any policy that is composed of those can be assumed provided already.
In the case that you need to create a custom enforcer, you can use the `CaveatEnforcer.sol` base class and write your own like this:
```solidity
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;
import "forge-std/Test.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ModeLib } from "@erc7579/lib/ModeLib.sol";
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
import { Execution, Caveat, Delegation, ModeCode } from "../../src/utils/Types.sol";
import { Counter } from "../utils/Counter.t.sol";
import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol";
import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol";
import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol";
contract AllowedMethodsEnforcerTest is CaveatEnforcerBaseTest {
////////////////////// State //////////////////////
AllowedMethodsEnforcer public allowedMethodsEnforcer;
ModeCode public mode = ModeLib.encodeSimpleSingle();
////////////////////// Set up //////////////////////
function setUp() public override {
super.setUp();
allowedMethodsEnforcer = new AllowedMethodsEnforcer();
vm.label(address(allowedMethodsEnforcer), "Allowed Methods Enforcer");
}
////////////////////// Valid cases //////////////////////
// should allow a method to be called when a single method is allowed
function test_singleMethodCanBeCalled() public {
// Create the execution that would be executed
Execution memory execution_ = Execution({
target: address(aliceDeleGatorCounter),
value: 0,
callData: abi.encodeWithSelector(Counter.increment.selector)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
// beforeHook, mimicking the behavior of Alice's DeleGator
vm.prank(address(delegationManager));
allowedMethodsEnforcer.beforeHook(
abi.encodePacked(Counter.increment.selector), hex"", mode, executionCallData_, keccak256(""), address(0), address(0)
);
}
// should allow a method to be called when a multiple methods are allowed
function test_multiMethodCanBeCalled() public {
// Create the execution that would be executed
Execution memory execution_ = Execution({
target: address(aliceDeleGatorCounter),
value: 0,
callData: abi.encodeWithSelector(Counter.increment.selector)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
// beforeHook, mimicking the behavior of Alice's DeleGator
vm.prank(address(delegationManager));
allowedMethodsEnforcer.beforeHook(
abi.encodePacked(Counter.setCount.selector, Ownable.renounceOwnership.selector, Counter.increment.selector),
hex"",
mode,
executionCallData_,
keccak256(""),
address(0),
address(0)
);
}
////////////////////// Invalid cases //////////////////////
// should FAIL to get terms info when passing an invalid terms length
function test_getTermsInfoFailsForInvalidLength() public {
vm.expectRevert("AllowedMethodsEnforcer:invalid-terms-length");
allowedMethodsEnforcer.getTermsInfo(bytes("1"));
}
// should FAIL if execution.callData length < 4
function test_notAllow_invalidExecutionLength() public {
// Create the execution that would be executed
Execution memory execution_ =
Execution({ target: address(aliceDeleGatorCounter), value: 0, callData: abi.encodePacked(true) });
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
// beforeHook, mimicking the behavior of Alice's DeleGator
vm.prank(address(delegationManager));
vm.expectRevert("AllowedMethodsEnforcer:invalid-execution-data-length");
allowedMethodsEnforcer.beforeHook(
abi.encodePacked(Counter.setCount.selector, Ownable.renounceOwnership.selector, Ownable.owner.selector),
hex"",
mode,
executionCallData_,
keccak256(""),
address(0),
address(0)
);
}
// should NOT allow a method to be called when the method is not allowed
function test_onlyApprovedMethodsCanBeCalled() public {
// Create the execution that would be executed
Execution memory execution_ = Execution({
target: address(aliceDeleGatorCounter),
value: 0,
callData: abi.encodeWithSelector(Counter.increment.selector)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
// beforeHook, mimicking the behavior of Alice's DeleGator
vm.prank(address(delegationManager));
vm.expectRevert("AllowedMethodsEnforcer:method-not-allowed");
allowedMethodsEnforcer.beforeHook(
abi.encodePacked(Counter.setCount.selector, Ownable.renounceOwnership.selector, Ownable.owner.selector),
hex"",
mode,
executionCallData_,
keccak256(""),
address(0),
address(0)
);
}
////////////////////// Integration //////////////////////
// should allow a method to be called when a single method is allowed Integration
function test_methodCanBeSingleMethodIntegration() public {
uint256 initialValue_ = aliceDeleGatorCounter.count();
// Create the execution that would be executed
Execution memory execution_ = Execution({
target: address(aliceDeleGatorCounter),
value: 0,
callData: abi.encodeWithSelector(Counter.increment.selector)
});
Caveat[] memory caveats_ = new Caveat[](1);
caveats_[0] =
Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(Counter.increment.selector) });
Delegation memory delegation_ = Delegation({
delegate: address(users.bob.deleGator),
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats_,
salt: 0,
signature: hex""
});
delegation_ = signDelegation(users.alice, delegation_);
// Execute Bob's UserOp
Delegation[] memory delegations_ = new Delegation[](1);
delegations_[0] = delegation_;
// Enforcer allows the delegation
invokeDelegation_UserOp(users.bob, delegations_, execution_);
// Get count
uint256 valueAfter_ = aliceDeleGatorCounter.count();
// Validate that the count has increased by 1
assertEq(valueAfter_, initialValue_ + 1);
// Enforcer allows to reuse the delegation
invokeDelegation_UserOp(users.bob, delegations_, execution_);
// Get final count
uint256 finalValue_ = aliceDeleGatorCounter.count();
// Validate that the count has increased again
assertEq(finalValue_, initialValue_ + 2);
}
// should NOT allow a method to be called when the method is not allowed Integration
function test_onlyApprovedMethodsCanBeCalledIntegration() public {
uint256 initialValue_ = aliceDeleGatorCounter.count();
// Create the execution that would be executed
Execution memory execution_ = Execution({
target: address(aliceDeleGatorCounter),
value: 0,
callData: abi.encodeWithSelector(Counter.increment.selector)
});
Caveat[] memory caveats_ = new Caveat[](1);
caveats_[0] = Caveat({
args: hex"",
enforcer: address(allowedMethodsEnforcer),
terms: abi.encodePacked(Counter.setCount.selector, Ownable.renounceOwnership.selector, Ownable.owner.selector)
});
Delegation memory delegation_ = Delegation({
delegate: address(users.bob.deleGator),
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats_,
salt: 0,
signature: hex""
});
delegation_ = signDelegation(users.alice, delegation_);
// Execute Bob's UserOp
Delegation[] memory delegations_ = new Delegation[](1);
delegations_[0] = delegation_;
// Enforcer allows the delegation
invokeDelegation_UserOp(users.bob, delegations_, execution_);
// Get final count
uint256 valueAfter_ = aliceDeleGatorCounter.count();
// Validate that the count has not changed
assertEq(valueAfter_, initialValue_);
}
function _getEnforcer() internal view override returns (ICaveatEnforcer) {
return ICaveatEnforcer(address(allowedMethodsEnforcer));
}
}
```
This example demonstrates a custom caveat enforcer that only permits approved methods to be called on. Here are some additional tips for working with the MetaMask Delegation Toolkit:
1. Simplify contract logic: The delegation framework allows you to move complex permission logic out of your main contracts. For example, instead of implementing complex access control in your main contract, you can use `onlyOwner` modifiers and handle permissions through delegations.
2. Leverage existing enforcers: Before creating a custom enforcer, check if any of the existing caveat enforcers (like AllowedCalldataEnforcer, BlockNumberEnforcer, ERC20TransferAmountEnforcer, etc.) can meet your needs.
3. Intent-based applications: When building intent-based apps:
a. Identify the resource being offered (e.g., NFT minting rights)
b. Determine the terms for offering the resource (e.g., payment, time limits)
c. Use or create appropriate caveat enforcers
4. Handle payments via delegations: NEVER use ERC20 allowances, `payable` functions, or permit() signatures. ONLY use delegations for receiving payments.
Assume that payments are usually handled by delegation managers (enforced outside of primary application logic), and that the primary application logic is focused on the intent to share a resource, without needing to consider the payment mechanics. As a result, it should be rare that a payment is required directly in a non-CaveatEnforcer contract. If payment is required by a contract (usually a CaveatEnforcer contract), never use ERC20 allowances, or token permit() signatures. The delegation framework is meant to be a general purpose alternative to these more constrained mechanisms. If a payment is required in exchange for something, have the payment be paid as a `Delegation` + a `delegationManager address`, which can then be redeemed using the ERC-7710 `redeemDelegations()` function on that `delegationManager`:
```solidity
function redeemDelegations(
bytes[] calldata _permissionContexts,
ModeCode[] calldata _modes,
bytes[] calldata _executionCallDatas
)
```
Where `_modes` is an array of `ModeCode` values, which are used to determine the behavior of the `redeemDelegations()` function (and mode 1 is a normal internal message call).
Calling a function using a permission context and delegation manager to performa simple send looks like this:
```solidity
import { ModeLib } from "@erc7579/lib/ModeLib.sol";
using ModeLib for ModeCode;
// Redeem ERC-7710 delegation
bytes[] memory permissionContexts_ = new bytes[](1);
permissionContexts_[0] = abi.encode(delegations_);
bytes[] memory executionCallDatas_ = new bytes[](1);
executionCallDatas_[0] = ExecutionLib.encodeSingle(recipient_, amount_, hex"");
ModeCode[] memory encodedModes_ = new ModeCode[](1);
encodedModes_[0] = ModeLib.encodeSimpleSingle();
// Attempt to redeem the delegation and make the payment
delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_);
```
5. Always use the delegation framework for invitations. Assume the owner can always let others use their account on their behalf with arbitrary terms.
6. Always ensure you use the `onlyOwner` modifier on any sensitive functions that should only be available to the owner. It's ok to leave this modifier off the transfer functions of a token or NFT contract. It's also ok to leave it off any function that is meant to be exposed as a public service.
7. After auto-generating a super simple contract, provide comments above each `onlyOwner` function that describes some expected/recommended delegation types for that function.