@btc-vision/btc-runtime 1.10.8 → 1.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +258 -137
- package/SECURITY.md +226 -0
- package/docs/README.md +614 -0
- package/docs/advanced/bitcoin-scripts.md +939 -0
- package/docs/advanced/cross-contract-calls.md +579 -0
- package/docs/advanced/plugins.md +1006 -0
- package/docs/advanced/quantum-resistance.md +660 -0
- package/docs/advanced/signature-verification.md +715 -0
- package/docs/api-reference/blockchain.md +729 -0
- package/docs/api-reference/events.md +642 -0
- package/docs/api-reference/op20.md +902 -0
- package/docs/api-reference/op721.md +819 -0
- package/docs/api-reference/safe-math.md +510 -0
- package/docs/api-reference/storage.md +840 -0
- package/docs/contracts/op-net-base.md +786 -0
- package/docs/contracts/op20-token.md +687 -0
- package/docs/contracts/op20s-signatures.md +614 -0
- package/docs/contracts/op721-nft.md +785 -0
- package/docs/contracts/reentrancy-guard.md +787 -0
- package/docs/core-concepts/blockchain-environment.md +724 -0
- package/docs/core-concepts/decorators.md +466 -0
- package/docs/core-concepts/events.md +652 -0
- package/docs/core-concepts/pointers.md +391 -0
- package/docs/core-concepts/security.md +473 -0
- package/docs/core-concepts/storage-system.md +969 -0
- package/docs/examples/basic-token.md +745 -0
- package/docs/examples/nft-with-reservations.md +1440 -0
- package/docs/examples/oracle-integration.md +1212 -0
- package/docs/examples/stablecoin.md +1180 -0
- package/docs/getting-started/first-contract.md +575 -0
- package/docs/getting-started/installation.md +384 -0
- package/docs/getting-started/project-structure.md +630 -0
- package/docs/storage/memory-maps.md +764 -0
- package/docs/storage/stored-arrays.md +778 -0
- package/docs/storage/stored-maps.md +758 -0
- package/docs/storage/stored-primitives.md +655 -0
- package/docs/types/address.md +773 -0
- package/docs/types/bytes-writer-reader.md +938 -0
- package/docs/types/calldata.md +744 -0
- package/docs/types/safe-math.md +446 -0
- package/package.json +52 -27
- package/runtime/memory/MapOfMap.ts +1 -0
- package/LICENSE.md +0 -21
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
# Stablecoin Example
|
|
2
|
+
|
|
3
|
+
A production-ready stablecoin implementation with role-based access control, pausability, blacklist functionality, and minter allowances.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This example demonstrates:
|
|
8
|
+
- Role-based access control (Admin, Minter, Pauser, Blacklister)
|
|
9
|
+
- Pausable transfers
|
|
10
|
+
- Address blacklisting
|
|
11
|
+
- Minter allowances with supply caps
|
|
12
|
+
- Decorators for ABI generation
|
|
13
|
+
- Detailed event logging
|
|
14
|
+
|
|
15
|
+
## Role-Based Access Control
|
|
16
|
+
|
|
17
|
+
Roles use bit flags (powers of 2) for efficient storage and checking:
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
---
|
|
21
|
+
config:
|
|
22
|
+
theme: dark
|
|
23
|
+
---
|
|
24
|
+
graph LR
|
|
25
|
+
subgraph "Role Enum - Powers of 2"
|
|
26
|
+
A["Role.ADMIN = 1<br/>(2^0 = 0001)"]
|
|
27
|
+
B["Role.MINTER = 2<br/>(2^1 = 0010)"]
|
|
28
|
+
C["Role.PAUSER = 4<br/>(2^2 = 0100)"]
|
|
29
|
+
D["Role.BLACKLISTER = 8<br/>(2^3 = 1000)"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph "Example: Account with Admin + Minter"
|
|
33
|
+
E["Roles = 0011 (binary)<br/>= 1 | 2 = 3"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
subgraph "Bitwise Operations"
|
|
37
|
+
F["Grant Role: SafeMath.or(currentRoles, role)"]
|
|
38
|
+
G["Revoke Role: SafeMath.and(currentRoles, SafeMath.xor(role, u256.Max))"]
|
|
39
|
+
H["Check Role: !SafeMath.and(roles, role).isZero()"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
A --> E
|
|
43
|
+
B --> E
|
|
44
|
+
E --> F
|
|
45
|
+
E --> G
|
|
46
|
+
E --> H
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Role Implementation
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Define roles as enum with bit flags
|
|
53
|
+
enum Role {
|
|
54
|
+
ADMIN = 1, // 2^0
|
|
55
|
+
MINTER = 2, // 2^1
|
|
56
|
+
PAUSER = 4, // 2^2
|
|
57
|
+
BLACKLISTER = 8 // 2^3
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check role before action
|
|
61
|
+
private onlyRole(role: u256): void {
|
|
62
|
+
if (!this.hasRole(Blockchain.tx.sender, role)) {
|
|
63
|
+
throw new Revert('AccessControl: missing role');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public hasRole(account: Address, role: u256): bool {
|
|
68
|
+
const roles = this._roles.get(account);
|
|
69
|
+
return !SafeMath.and(roles, role).isZero();
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Solidity Comparison:**
|
|
74
|
+
|
|
75
|
+
```solidity
|
|
76
|
+
// Solidity - OpenZeppelin AccessControl
|
|
77
|
+
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
|
78
|
+
|
|
79
|
+
function hasRole(bytes32 role, address account) public view returns (bool) {
|
|
80
|
+
return _roles[role].members[account];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
modifier onlyRole(bytes32 role) {
|
|
84
|
+
require(hasRole(role, msg.sender), "AccessControl: missing role");
|
|
85
|
+
_;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// OPNet - Bit flags for efficiency
|
|
89
|
+
enum Role { ADMIN = 1, MINTER = 2, PAUSER = 4, BLACKLISTER = 8 }
|
|
90
|
+
|
|
91
|
+
public hasRole(account: Address, role: u256): bool {
|
|
92
|
+
const roles = this._roles.get(account);
|
|
93
|
+
return !SafeMath.and(roles, role).isZero();
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Grant/Revoke Flow
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
---
|
|
101
|
+
config:
|
|
102
|
+
theme: dark
|
|
103
|
+
---
|
|
104
|
+
flowchart LR
|
|
105
|
+
A["👤 User submits grantRole TX"] --> B{Has ADMIN role?}
|
|
106
|
+
B -->|No| C[Revert: Missing role]
|
|
107
|
+
B -->|Yes| D[Read current roles]
|
|
108
|
+
D --> E[OR roles together]
|
|
109
|
+
E --> F[Write to storage]
|
|
110
|
+
F --> G[Emit event]
|
|
111
|
+
G --> H[TX Success]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Role Hierarchy
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
Admin (Role.ADMIN = 1)
|
|
118
|
+
- Can grant/revoke all roles
|
|
119
|
+
- Can update master minter
|
|
120
|
+
- Has emergency powers
|
|
121
|
+
|
|
122
|
+
Master Minter
|
|
123
|
+
- Configure minter allowances
|
|
124
|
+
- Add new minters
|
|
125
|
+
- Remove minters
|
|
126
|
+
|
|
127
|
+
Minter (Role.MINTER = 2)
|
|
128
|
+
- Mint up to allowance
|
|
129
|
+
- Burn own tokens
|
|
130
|
+
|
|
131
|
+
Pauser (Role.PAUSER = 4)
|
|
132
|
+
- Pause all transfers
|
|
133
|
+
- Unpause
|
|
134
|
+
|
|
135
|
+
Blacklister (Role.BLACKLISTER = 8)
|
|
136
|
+
- Add addresses to blacklist
|
|
137
|
+
- Remove from blacklist
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Pausable Functionality
|
|
141
|
+
|
|
142
|
+
The contract can be paused to block all transfers:
|
|
143
|
+
|
|
144
|
+
```mermaid
|
|
145
|
+
---
|
|
146
|
+
config:
|
|
147
|
+
theme: dark
|
|
148
|
+
---
|
|
149
|
+
stateDiagram-v2
|
|
150
|
+
[*] --> Normal: Deploy Contract
|
|
151
|
+
Normal --> Paused: pause() TX by Role.PAUSER
|
|
152
|
+
Paused --> Normal: unpause() TX by Role.PAUSER
|
|
153
|
+
|
|
154
|
+
state Normal {
|
|
155
|
+
[*] --> AllowTransfers
|
|
156
|
+
AllowTransfers --> AllowMinting
|
|
157
|
+
AllowMinting --> AllowBurning
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
state Paused {
|
|
161
|
+
[*] --> BlockTransfers
|
|
162
|
+
BlockTransfers --> BlockMinting
|
|
163
|
+
BlockMinting --> BlockBurning
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
note right of Normal
|
|
167
|
+
All token operations allowed
|
|
168
|
+
transfers, mints, burns work
|
|
169
|
+
Storage updates permitted
|
|
170
|
+
end note
|
|
171
|
+
|
|
172
|
+
note right of Paused
|
|
173
|
+
Only view functions work
|
|
174
|
+
All state-changing ops revert
|
|
175
|
+
Users must wait for unpause
|
|
176
|
+
end note
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Pausable Implementation
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
private whenNotPaused(): void {
|
|
183
|
+
if (this._paused.value) {
|
|
184
|
+
throw new Revert('Pausable: paused');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@method()
|
|
189
|
+
@emit('Paused')
|
|
190
|
+
public pause(_calldata: Calldata): BytesWriter {
|
|
191
|
+
this.onlyRole(u256.fromU64(Role.PAUSER));
|
|
192
|
+
this._paused.value = true;
|
|
193
|
+
this.emitEvent(new Paused(Blockchain.tx.sender));
|
|
194
|
+
return new BytesWriter(0);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Solidity Comparison:**
|
|
199
|
+
|
|
200
|
+
```solidity
|
|
201
|
+
// Solidity - OpenZeppelin Pausable
|
|
202
|
+
modifier whenNotPaused() {
|
|
203
|
+
require(!paused(), "Pausable: paused");
|
|
204
|
+
_;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function pause() external onlyRole(PAUSER_ROLE) {
|
|
208
|
+
_pause();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// OPNet
|
|
212
|
+
private whenNotPaused(): void {
|
|
213
|
+
if (this._paused.value) {
|
|
214
|
+
throw new Revert('Pausable: paused');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Blacklist System
|
|
220
|
+
|
|
221
|
+
Addresses can be blacklisted to prevent transfers. Each address exists in one of two states:
|
|
222
|
+
|
|
223
|
+
```mermaid
|
|
224
|
+
---
|
|
225
|
+
config:
|
|
226
|
+
theme: dark
|
|
227
|
+
---
|
|
228
|
+
stateDiagram-v2
|
|
229
|
+
[*] --> Active: Address First Used
|
|
230
|
+
Active --> Blacklisted: blacklist(address) by BLACKLISTER
|
|
231
|
+
Blacklisted --> Active: unBlacklist(address) by BLACKLISTER
|
|
232
|
+
|
|
233
|
+
state Active {
|
|
234
|
+
[*] --> CanTransfer
|
|
235
|
+
CanTransfer --> CanReceive
|
|
236
|
+
CanReceive --> CanMint
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
state Blacklisted {
|
|
240
|
+
[*] --> TransferBlocked
|
|
241
|
+
TransferBlocked --> ReceiveBlocked
|
|
242
|
+
ReceiveBlocked --> MintBlocked
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
note right of Active
|
|
246
|
+
Address can:
|
|
247
|
+
- Send tokens
|
|
248
|
+
- Receive tokens
|
|
249
|
+
- Be minted to
|
|
250
|
+
end note
|
|
251
|
+
|
|
252
|
+
note right of Blacklisted
|
|
253
|
+
Address cannot:
|
|
254
|
+
- Send tokens (reverts)
|
|
255
|
+
- Receive tokens (reverts)
|
|
256
|
+
- Be minted to (reverts)
|
|
257
|
+
Balance is frozen
|
|
258
|
+
end note
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```mermaid
|
|
262
|
+
---
|
|
263
|
+
config:
|
|
264
|
+
theme: dark
|
|
265
|
+
---
|
|
266
|
+
flowchart LR
|
|
267
|
+
A["👤 User submits transfer TX"] --> B{Contract paused?}
|
|
268
|
+
B -->|Yes| C[Revert: Paused]
|
|
269
|
+
B -->|No| D{Blacklisted?}
|
|
270
|
+
D -->|Yes| E[Revert: Blacklisted]
|
|
271
|
+
D -->|No| F{Sufficient balance?}
|
|
272
|
+
F -->|No| G[Revert: Insufficient balance]
|
|
273
|
+
F -->|Yes| H[Update balances]
|
|
274
|
+
H --> I[Emit event]
|
|
275
|
+
I --> J[TX Success]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Blacklist Implementation
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
private notBlacklisted(account: Address): void {
|
|
282
|
+
// AddressMemoryMap.get() returns u256; non-zero means blacklisted
|
|
283
|
+
if (!this._blacklist.get(account).isZero()) {
|
|
284
|
+
throw new Revert('Blacklisted');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
public override transfer(calldata: Calldata): BytesWriter {
|
|
289
|
+
this.whenNotPaused();
|
|
290
|
+
this.notBlacklisted(Blockchain.tx.sender);
|
|
291
|
+
|
|
292
|
+
const to = calldata.readAddress();
|
|
293
|
+
this.notBlacklisted(to);
|
|
294
|
+
|
|
295
|
+
// Continue with transfer...
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Minter Allowance System
|
|
300
|
+
|
|
301
|
+
Each minter has a limited supply they can mint. The master minter configures allowances for each minter:
|
|
302
|
+
|
|
303
|
+
```mermaid
|
|
304
|
+
sequenceDiagram
|
|
305
|
+
participant MasterMinter as 👤 Master Minter
|
|
306
|
+
participant BTC as Bitcoin Network
|
|
307
|
+
participant Contract as Contract Execution
|
|
308
|
+
participant Minter as 👤 Minter
|
|
309
|
+
participant Storage as Storage Layer
|
|
310
|
+
|
|
311
|
+
MasterMinter->>BTC: Submit configureMinter(minter, allowance) TX
|
|
312
|
+
BTC->>Contract: Execute configureMinter
|
|
313
|
+
Contract->>Contract: Check caller is master minter
|
|
314
|
+
Contract->>Storage: Write Role.MINTER if needed
|
|
315
|
+
Contract->>Storage: Write minter allowance
|
|
316
|
+
Storage-->>Contract: Success
|
|
317
|
+
Contract-->>BTC: Success
|
|
318
|
+
BTC-->>MasterMinter: TX Confirmed
|
|
319
|
+
|
|
320
|
+
Note over Minter,Storage: Later: Minter wants to mint
|
|
321
|
+
|
|
322
|
+
Minter->>BTC: Submit mint(to, amount) TX
|
|
323
|
+
BTC->>Contract: Execute mint
|
|
324
|
+
Contract->>Contract: Check Role.MINTER
|
|
325
|
+
Contract->>Contract: Check not paused
|
|
326
|
+
Contract->>Contract: Check blacklist (to + minter)
|
|
327
|
+
Contract->>Storage: Read minter allowance
|
|
328
|
+
Storage-->>Contract: currentAllowance
|
|
329
|
+
|
|
330
|
+
alt amount > allowance
|
|
331
|
+
Contract-->>BTC: Revert: Allowance exceeded
|
|
332
|
+
BTC-->>Minter: TX Failed
|
|
333
|
+
else amount <= allowance
|
|
334
|
+
Contract->>Storage: Write allowance -= amount
|
|
335
|
+
Contract->>Storage: Write _mint(to, amount)
|
|
336
|
+
Contract->>Contract: Emit Mint event
|
|
337
|
+
Contract-->>BTC: Success
|
|
338
|
+
BTC-->>Minter: TX Confirmed
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
Note over Contract,Storage: Allowance tracks remaining mint capacity
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Minter Allowance Implementation
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
@method(
|
|
348
|
+
{ name: 'minter', type: ABIDataTypes.ADDRESS },
|
|
349
|
+
{ name: 'allowance', type: ABIDataTypes.UINT256 },
|
|
350
|
+
)
|
|
351
|
+
public configureMinter(calldata: Calldata): BytesWriter {
|
|
352
|
+
this.onlyMasterMinter();
|
|
353
|
+
const minter = calldata.readAddress();
|
|
354
|
+
const allowance = calldata.readU256(); // Max they can mint
|
|
355
|
+
// ...
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Complete Implementation
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
363
|
+
import {
|
|
364
|
+
OP20,
|
|
365
|
+
OP20InitParameters,
|
|
366
|
+
Blockchain,
|
|
367
|
+
Address,
|
|
368
|
+
Calldata,
|
|
369
|
+
BytesWriter,
|
|
370
|
+
SafeMath,
|
|
371
|
+
Revert,
|
|
372
|
+
NetEvent,
|
|
373
|
+
StoredBoolean,
|
|
374
|
+
StoredAddress,
|
|
375
|
+
AddressMemoryMap,
|
|
376
|
+
ABIDataTypes,
|
|
377
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
378
|
+
|
|
379
|
+
// Role enum - MUST be powers of 2 for bitwise operations
|
|
380
|
+
enum Role {
|
|
381
|
+
ADMIN = 1, // 2^0
|
|
382
|
+
MINTER = 2, // 2^1
|
|
383
|
+
PAUSER = 4, // 2^2
|
|
384
|
+
BLACKLISTER = 8 // 2^3
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Custom events
|
|
388
|
+
class RoleGranted extends NetEvent {
|
|
389
|
+
public constructor(
|
|
390
|
+
public readonly role: u256,
|
|
391
|
+
public readonly account: Address,
|
|
392
|
+
public readonly sender: Address
|
|
393
|
+
) {
|
|
394
|
+
super('RoleGranted');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
398
|
+
writer.writeU256(this.role);
|
|
399
|
+
writer.writeAddress(this.account);
|
|
400
|
+
writer.writeAddress(this.sender);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
class RoleRevoked extends NetEvent {
|
|
405
|
+
public constructor(
|
|
406
|
+
public readonly role: u256,
|
|
407
|
+
public readonly account: Address,
|
|
408
|
+
public readonly sender: Address
|
|
409
|
+
) {
|
|
410
|
+
super('RoleRevoked');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
414
|
+
writer.writeU256(this.role);
|
|
415
|
+
writer.writeAddress(this.account);
|
|
416
|
+
writer.writeAddress(this.sender);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
class Blacklisted extends NetEvent {
|
|
421
|
+
public constructor(public readonly account: Address) {
|
|
422
|
+
super('Blacklisted');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
426
|
+
writer.writeAddress(this.account);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
class UnBlacklisted extends NetEvent {
|
|
431
|
+
public constructor(public readonly account: Address) {
|
|
432
|
+
super('UnBlacklisted');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
436
|
+
writer.writeAddress(this.account);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
class Paused extends NetEvent {
|
|
441
|
+
public constructor(public readonly account: Address) {
|
|
442
|
+
super('Paused');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
446
|
+
writer.writeAddress(this.account);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
class Unpaused extends NetEvent {
|
|
451
|
+
public constructor(public readonly account: Address) {
|
|
452
|
+
super('Unpaused');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
456
|
+
writer.writeAddress(this.account);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@final
|
|
461
|
+
export class Stablecoin extends OP20 {
|
|
462
|
+
// Access control storage
|
|
463
|
+
private rolesPointer: u16 = Blockchain.nextPointer;
|
|
464
|
+
private masterMinterPointer: u16 = Blockchain.nextPointer;
|
|
465
|
+
|
|
466
|
+
// Pausable storage
|
|
467
|
+
private pausedPointer: u16 = Blockchain.nextPointer;
|
|
468
|
+
|
|
469
|
+
// Blacklist storage
|
|
470
|
+
private blacklistPointer: u16 = Blockchain.nextPointer;
|
|
471
|
+
|
|
472
|
+
// Minter allowances
|
|
473
|
+
private minterAllowancePointer: u16 = Blockchain.nextPointer;
|
|
474
|
+
|
|
475
|
+
// Stored values
|
|
476
|
+
private _roles: AddressMemoryMap;
|
|
477
|
+
private _masterMinter: StoredAddress;
|
|
478
|
+
private _paused: StoredBoolean;
|
|
479
|
+
private _blacklist: AddressMemoryMap;
|
|
480
|
+
private _minterAllowance: AddressMemoryMap;
|
|
481
|
+
|
|
482
|
+
public constructor() {
|
|
483
|
+
super();
|
|
484
|
+
|
|
485
|
+
this._roles = new AddressMemoryMap(this.rolesPointer);
|
|
486
|
+
this._masterMinter = new StoredAddress(this.masterMinterPointer, Address.zero());
|
|
487
|
+
this._paused = new StoredBoolean(this.pausedPointer, false);
|
|
488
|
+
this._blacklist = new AddressMemoryMap(this.blacklistPointer);
|
|
489
|
+
this._minterAllowance = new AddressMemoryMap(this.minterAllowancePointer);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public override onDeployment(calldata: Calldata): void {
|
|
493
|
+
const name = calldata.readString();
|
|
494
|
+
const symbol = calldata.readString();
|
|
495
|
+
const admin = calldata.readAddress();
|
|
496
|
+
const masterMinter = calldata.readAddress();
|
|
497
|
+
|
|
498
|
+
// Initialize as stablecoin (no max supply, 6 decimals typical for USD)
|
|
499
|
+
this.instantiate(new OP20InitParameters(
|
|
500
|
+
u256.Max, // No max supply
|
|
501
|
+
6, // USDC-style decimals
|
|
502
|
+
name,
|
|
503
|
+
symbol
|
|
504
|
+
));
|
|
505
|
+
|
|
506
|
+
// Set up initial roles
|
|
507
|
+
this._grantRole(admin, u256.fromU64(Role.ADMIN));
|
|
508
|
+
this._grantRole(admin, u256.fromU64(Role.PAUSER));
|
|
509
|
+
this._grantRole(admin, u256.fromU64(Role.BLACKLISTER));
|
|
510
|
+
this._masterMinter.value = masterMinter;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ============ MODIFIERS ============
|
|
514
|
+
|
|
515
|
+
private onlyRole(role: u256): void {
|
|
516
|
+
if (!this.hasRole(Blockchain.tx.sender, role)) {
|
|
517
|
+
throw new Revert('AccessControl: missing role');
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private whenNotPaused(): void {
|
|
522
|
+
if (this._paused.value) {
|
|
523
|
+
throw new Revert('Pausable: paused');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private notBlacklisted(account: Address): void {
|
|
528
|
+
// AddressMemoryMap.get() returns u256; non-zero means blacklisted
|
|
529
|
+
if (!this._blacklist.get(account).isZero()) {
|
|
530
|
+
throw new Revert('Blacklisted');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private onlyMasterMinter(): void {
|
|
535
|
+
if (!Blockchain.tx.sender.equals(this._masterMinter.value)) {
|
|
536
|
+
throw new Revert('Caller is not master minter');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============ ROLE MANAGEMENT ============
|
|
541
|
+
|
|
542
|
+
private _grantRole(account: Address, role: u256): void {
|
|
543
|
+
const currentRoles = this._roles.get(account);
|
|
544
|
+
// Use SafeMath.or for bitwise OR on u256
|
|
545
|
+
const newRoles = SafeMath.or(currentRoles, role);
|
|
546
|
+
this._roles.set(account, newRoles);
|
|
547
|
+
|
|
548
|
+
this.emitEvent(new RoleGranted(role, account, Blockchain.tx.sender));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private _revokeRole(account: Address, role: u256): void {
|
|
552
|
+
const currentRoles = this._roles.get(account);
|
|
553
|
+
// Use SafeMath.xor to invert, then SafeMath.and to clear bits
|
|
554
|
+
const invertedRole = SafeMath.xor(role, u256.Max);
|
|
555
|
+
const newRoles = SafeMath.and(currentRoles, invertedRole);
|
|
556
|
+
this._roles.set(account, newRoles);
|
|
557
|
+
|
|
558
|
+
this.emitEvent(new RoleRevoked(role, account, Blockchain.tx.sender));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
public hasRole(account: Address, role: u256): bool {
|
|
562
|
+
const roles = this._roles.get(account);
|
|
563
|
+
// Use SafeMath.and for bitwise AND on u256
|
|
564
|
+
return !SafeMath.and(roles, role).isZero();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
@method(
|
|
568
|
+
{ name: 'account', type: ABIDataTypes.ADDRESS },
|
|
569
|
+
{ name: 'role', type: ABIDataTypes.UINT256 },
|
|
570
|
+
)
|
|
571
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
572
|
+
@emit('RoleGranted')
|
|
573
|
+
public grantRole(calldata: Calldata): BytesWriter {
|
|
574
|
+
this.onlyRole(u256.fromU64(Role.ADMIN));
|
|
575
|
+
|
|
576
|
+
const account = calldata.readAddress();
|
|
577
|
+
const role = calldata.readU256();
|
|
578
|
+
|
|
579
|
+
this._grantRole(account, role);
|
|
580
|
+
|
|
581
|
+
return new BytesWriter(0);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
@method(
|
|
585
|
+
{ name: 'account', type: ABIDataTypes.ADDRESS },
|
|
586
|
+
{ name: 'role', type: ABIDataTypes.UINT256 },
|
|
587
|
+
)
|
|
588
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
589
|
+
@emit('RoleRevoked')
|
|
590
|
+
public revokeRole(calldata: Calldata): BytesWriter {
|
|
591
|
+
this.onlyRole(u256.fromU64(Role.ADMIN));
|
|
592
|
+
|
|
593
|
+
const account = calldata.readAddress();
|
|
594
|
+
const role = calldata.readU256();
|
|
595
|
+
|
|
596
|
+
this._revokeRole(account, role);
|
|
597
|
+
|
|
598
|
+
return new BytesWriter(0);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ============ MINTING ============
|
|
602
|
+
|
|
603
|
+
@method(
|
|
604
|
+
{ name: 'minter', type: ABIDataTypes.ADDRESS },
|
|
605
|
+
{ name: 'allowance', type: ABIDataTypes.UINT256 },
|
|
606
|
+
)
|
|
607
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
608
|
+
@emit('MinterConfigured')
|
|
609
|
+
public configureMinter(calldata: Calldata): BytesWriter {
|
|
610
|
+
this.onlyMasterMinter();
|
|
611
|
+
|
|
612
|
+
const minter = calldata.readAddress();
|
|
613
|
+
const allowance = calldata.readU256();
|
|
614
|
+
|
|
615
|
+
// Grant minter role if new
|
|
616
|
+
if (!this.hasRole(minter, u256.fromU64(Role.MINTER))) {
|
|
617
|
+
this._grantRole(minter, u256.fromU64(Role.MINTER));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Set allowance
|
|
621
|
+
this._minterAllowance.set(minter, allowance);
|
|
622
|
+
|
|
623
|
+
return new BytesWriter(0);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
@method({ name: 'minter', type: ABIDataTypes.ADDRESS })
|
|
627
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
628
|
+
@emit('MinterRemoved')
|
|
629
|
+
public removeMinter(calldata: Calldata): BytesWriter {
|
|
630
|
+
this.onlyMasterMinter();
|
|
631
|
+
|
|
632
|
+
const minter = calldata.readAddress();
|
|
633
|
+
|
|
634
|
+
this._revokeRole(minter, u256.fromU64(Role.MINTER));
|
|
635
|
+
this._minterAllowance.set(minter, u256.Zero);
|
|
636
|
+
|
|
637
|
+
return new BytesWriter(0);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
@method(
|
|
641
|
+
{ name: 'to', type: ABIDataTypes.ADDRESS },
|
|
642
|
+
{ name: 'amount', type: ABIDataTypes.UINT256 },
|
|
643
|
+
)
|
|
644
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
645
|
+
@emit('Mint')
|
|
646
|
+
public mint(calldata: Calldata): BytesWriter {
|
|
647
|
+
this.onlyRole(u256.fromU64(Role.MINTER));
|
|
648
|
+
this.whenNotPaused();
|
|
649
|
+
|
|
650
|
+
const to = calldata.readAddress();
|
|
651
|
+
const amount = calldata.readU256();
|
|
652
|
+
const minter = Blockchain.tx.sender;
|
|
653
|
+
|
|
654
|
+
// Check blacklist
|
|
655
|
+
this.notBlacklisted(to);
|
|
656
|
+
this.notBlacklisted(minter);
|
|
657
|
+
|
|
658
|
+
// Check and update allowance
|
|
659
|
+
const allowance = this._minterAllowance.get(minter);
|
|
660
|
+
if (allowance < amount) {
|
|
661
|
+
throw new Revert('Minter allowance exceeded');
|
|
662
|
+
}
|
|
663
|
+
this._minterAllowance.set(minter, SafeMath.sub(allowance, amount));
|
|
664
|
+
|
|
665
|
+
// Mint
|
|
666
|
+
this._mint(to, amount);
|
|
667
|
+
|
|
668
|
+
return new BytesWriter(0);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
@method({ name: 'amount', type: ABIDataTypes.UINT256 })
|
|
672
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
673
|
+
@emit('Burn')
|
|
674
|
+
public burn(calldata: Calldata): BytesWriter {
|
|
675
|
+
this.onlyRole(u256.fromU64(Role.MINTER));
|
|
676
|
+
this.whenNotPaused();
|
|
677
|
+
|
|
678
|
+
const amount = calldata.readU256();
|
|
679
|
+
const burner = Blockchain.tx.sender;
|
|
680
|
+
|
|
681
|
+
this.notBlacklisted(burner);
|
|
682
|
+
|
|
683
|
+
this._burn(burner, amount);
|
|
684
|
+
|
|
685
|
+
return new BytesWriter(0);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ============ PAUSABLE ============
|
|
689
|
+
|
|
690
|
+
@method()
|
|
691
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
692
|
+
@emit('Paused')
|
|
693
|
+
public pause(_calldata: Calldata): BytesWriter {
|
|
694
|
+
this.onlyRole(u256.fromU64(Role.PAUSER));
|
|
695
|
+
|
|
696
|
+
this._paused.value = true;
|
|
697
|
+
this.emitEvent(new Paused(Blockchain.tx.sender));
|
|
698
|
+
|
|
699
|
+
return new BytesWriter(0);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
@method()
|
|
703
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
704
|
+
@emit('Unpaused')
|
|
705
|
+
public unpause(_calldata: Calldata): BytesWriter {
|
|
706
|
+
this.onlyRole(u256.fromU64(Role.PAUSER));
|
|
707
|
+
|
|
708
|
+
this._paused.value = false;
|
|
709
|
+
this.emitEvent(new Unpaused(Blockchain.tx.sender));
|
|
710
|
+
|
|
711
|
+
return new BytesWriter(0);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ============ BLACKLIST ============
|
|
715
|
+
|
|
716
|
+
@method({ name: 'account', type: ABIDataTypes.ADDRESS })
|
|
717
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
718
|
+
@emit('Blacklisted')
|
|
719
|
+
public blacklist(calldata: Calldata): BytesWriter {
|
|
720
|
+
this.onlyRole(u256.fromU64(Role.BLACKLISTER));
|
|
721
|
+
|
|
722
|
+
const account = calldata.readAddress();
|
|
723
|
+
// AddressMemoryMap stores u256; use u256.One for true
|
|
724
|
+
this._blacklist.set(account, u256.One);
|
|
725
|
+
|
|
726
|
+
this.emitEvent(new Blacklisted(account));
|
|
727
|
+
|
|
728
|
+
return new BytesWriter(0);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
@method({ name: 'account', type: ABIDataTypes.ADDRESS })
|
|
732
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
733
|
+
@emit('UnBlacklisted')
|
|
734
|
+
public unBlacklist(calldata: Calldata): BytesWriter {
|
|
735
|
+
this.onlyRole(u256.fromU64(Role.BLACKLISTER));
|
|
736
|
+
|
|
737
|
+
const account = calldata.readAddress();
|
|
738
|
+
// AddressMemoryMap stores u256; use u256.Zero for false
|
|
739
|
+
this._blacklist.set(account, u256.Zero);
|
|
740
|
+
|
|
741
|
+
this.emitEvent(new UnBlacklisted(account));
|
|
742
|
+
|
|
743
|
+
return new BytesWriter(0);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ============ OVERRIDE TRANSFERS ============
|
|
747
|
+
|
|
748
|
+
@method(
|
|
749
|
+
{ name: 'to', type: ABIDataTypes.ADDRESS },
|
|
750
|
+
{ name: 'amount', type: ABIDataTypes.UINT256 },
|
|
751
|
+
)
|
|
752
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
753
|
+
@emit('Transfer')
|
|
754
|
+
public override transfer(calldata: Calldata): BytesWriter {
|
|
755
|
+
this.whenNotPaused();
|
|
756
|
+
this.notBlacklisted(Blockchain.tx.sender);
|
|
757
|
+
|
|
758
|
+
const to = calldata.readAddress();
|
|
759
|
+
this.notBlacklisted(to);
|
|
760
|
+
|
|
761
|
+
// Re-read to pass to parent
|
|
762
|
+
const fullCalldata = new Calldata(calldata.buffer);
|
|
763
|
+
return super.transfer(fullCalldata);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@method(
|
|
767
|
+
{ name: 'from', type: ABIDataTypes.ADDRESS },
|
|
768
|
+
{ name: 'to', type: ABIDataTypes.ADDRESS },
|
|
769
|
+
{ name: 'amount', type: ABIDataTypes.UINT256 },
|
|
770
|
+
)
|
|
771
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
772
|
+
@emit('Transfer')
|
|
773
|
+
public override transferFrom(calldata: Calldata): BytesWriter {
|
|
774
|
+
this.whenNotPaused();
|
|
775
|
+
|
|
776
|
+
const from = calldata.readAddress();
|
|
777
|
+
const to = calldata.readAddress();
|
|
778
|
+
|
|
779
|
+
this.notBlacklisted(Blockchain.tx.sender);
|
|
780
|
+
this.notBlacklisted(from);
|
|
781
|
+
this.notBlacklisted(to);
|
|
782
|
+
|
|
783
|
+
// Re-read to pass to parent
|
|
784
|
+
const fullCalldata = new Calldata(calldata.buffer);
|
|
785
|
+
return super.transferFrom(fullCalldata);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ============ VIEW FUNCTIONS ============
|
|
789
|
+
|
|
790
|
+
@method()
|
|
791
|
+
@returns({ name: 'paused', type: ABIDataTypes.BOOL })
|
|
792
|
+
public isPaused(_calldata: Calldata): BytesWriter {
|
|
793
|
+
const writer = new BytesWriter(1);
|
|
794
|
+
writer.writeBoolean(this._paused.value);
|
|
795
|
+
return writer;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
@method({ name: 'account', type: ABIDataTypes.ADDRESS })
|
|
799
|
+
@returns({ name: 'blacklisted', type: ABIDataTypes.BOOL })
|
|
800
|
+
public isBlacklisted(calldata: Calldata): BytesWriter {
|
|
801
|
+
const account = calldata.readAddress();
|
|
802
|
+
|
|
803
|
+
const writer = new BytesWriter(1);
|
|
804
|
+
// AddressMemoryMap.get() returns u256; convert to boolean
|
|
805
|
+
writer.writeBoolean(!this._blacklist.get(account).isZero());
|
|
806
|
+
return writer;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
@method(
|
|
810
|
+
{ name: 'account', type: ABIDataTypes.ADDRESS },
|
|
811
|
+
{ name: 'role', type: ABIDataTypes.UINT256 },
|
|
812
|
+
)
|
|
813
|
+
@returns({ name: 'hasRole', type: ABIDataTypes.BOOL })
|
|
814
|
+
public checkHasRole(calldata: Calldata): BytesWriter {
|
|
815
|
+
const account = calldata.readAddress();
|
|
816
|
+
const role = calldata.readU256();
|
|
817
|
+
|
|
818
|
+
const writer = new BytesWriter(1);
|
|
819
|
+
writer.writeBoolean(this.hasRole(account, role));
|
|
820
|
+
return writer;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
@method({ name: 'minter', type: ABIDataTypes.ADDRESS })
|
|
824
|
+
@returns({ name: 'allowance', type: ABIDataTypes.UINT256 })
|
|
825
|
+
public minterAllowance(calldata: Calldata): BytesWriter {
|
|
826
|
+
const minter = calldata.readAddress();
|
|
827
|
+
|
|
828
|
+
const writer = new BytesWriter(32);
|
|
829
|
+
writer.writeU256(this._minterAllowance.get(minter));
|
|
830
|
+
return writer;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
@method()
|
|
834
|
+
@returns({ name: 'masterMinter', type: ABIDataTypes.ADDRESS })
|
|
835
|
+
public getMasterMinter(_calldata: Calldata): BytesWriter {
|
|
836
|
+
const writer = new BytesWriter(32);
|
|
837
|
+
writer.writeAddress(this._masterMinter.value);
|
|
838
|
+
return writer;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
## Key Patterns Summary
|
|
844
|
+
|
|
845
|
+
### Bitwise Operations on u256
|
|
846
|
+
|
|
847
|
+
Use `SafeMath` methods for bitwise operations:
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
// Grant role (OR)
|
|
851
|
+
const newRoles = SafeMath.or(currentRoles, role);
|
|
852
|
+
|
|
853
|
+
// Revoke role (AND with inverted mask)
|
|
854
|
+
const invertedRole = SafeMath.xor(role, u256.Max);
|
|
855
|
+
const newRoles = SafeMath.and(currentRoles, invertedRole);
|
|
856
|
+
|
|
857
|
+
// Check role (AND)
|
|
858
|
+
const hasRole = !SafeMath.and(roles, role).isZero();
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
## Solidity Equivalent
|
|
862
|
+
|
|
863
|
+
For developers familiar with Solidity, here is the equivalent implementation using OpenZeppelin's ERC20Pausable and AccessControl:
|
|
864
|
+
|
|
865
|
+
```solidity
|
|
866
|
+
// SPDX-License-Identifier: MIT
|
|
867
|
+
pragma solidity ^0.8.20;
|
|
868
|
+
|
|
869
|
+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
870
|
+
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
|
|
871
|
+
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
872
|
+
|
|
873
|
+
contract Stablecoin is ERC20, ERC20Pausable, AccessControl {
|
|
874
|
+
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
|
875
|
+
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
|
|
876
|
+
bytes32 public constant BLACKLISTER_ROLE = keccak256("BLACKLISTER_ROLE");
|
|
877
|
+
|
|
878
|
+
address public masterMinter;
|
|
879
|
+
mapping(address => bool) public blacklisted;
|
|
880
|
+
mapping(address => uint256) public minterAllowance;
|
|
881
|
+
|
|
882
|
+
event Blacklisted(address indexed account);
|
|
883
|
+
event UnBlacklisted(address indexed account);
|
|
884
|
+
event MinterConfigured(address indexed minter, uint256 allowance);
|
|
885
|
+
event MinterRemoved(address indexed minter);
|
|
886
|
+
|
|
887
|
+
modifier notBlacklisted(address account) {
|
|
888
|
+
require(!blacklisted[account], "Blacklisted");
|
|
889
|
+
_;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
constructor(
|
|
893
|
+
string memory name,
|
|
894
|
+
string memory symbol,
|
|
895
|
+
address admin,
|
|
896
|
+
address _masterMinter
|
|
897
|
+
) ERC20(name, symbol) {
|
|
898
|
+
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
899
|
+
_grantRole(PAUSER_ROLE, admin);
|
|
900
|
+
_grantRole(BLACKLISTER_ROLE, admin);
|
|
901
|
+
masterMinter = _masterMinter;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function decimals() public pure override returns (uint8) {
|
|
905
|
+
return 6; // USDC-style
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// ============ MINTING ============
|
|
909
|
+
|
|
910
|
+
function configureMinter(address minter, uint256 allowance) external {
|
|
911
|
+
require(msg.sender == masterMinter, "Caller is not master minter");
|
|
912
|
+
if (!hasRole(MINTER_ROLE, minter)) {
|
|
913
|
+
_grantRole(MINTER_ROLE, minter);
|
|
914
|
+
}
|
|
915
|
+
minterAllowance[minter] = allowance;
|
|
916
|
+
emit MinterConfigured(minter, allowance);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function removeMinter(address minter) external {
|
|
920
|
+
require(msg.sender == masterMinter, "Caller is not master minter");
|
|
921
|
+
_revokeRole(MINTER_ROLE, minter);
|
|
922
|
+
minterAllowance[minter] = 0;
|
|
923
|
+
emit MinterRemoved(minter);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function mint(address to, uint256 amount)
|
|
927
|
+
external
|
|
928
|
+
onlyRole(MINTER_ROLE)
|
|
929
|
+
whenNotPaused
|
|
930
|
+
notBlacklisted(msg.sender)
|
|
931
|
+
notBlacklisted(to)
|
|
932
|
+
{
|
|
933
|
+
require(minterAllowance[msg.sender] >= amount, "Minter allowance exceeded");
|
|
934
|
+
minterAllowance[msg.sender] -= amount;
|
|
935
|
+
_mint(to, amount);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function burn(uint256 amount)
|
|
939
|
+
external
|
|
940
|
+
onlyRole(MINTER_ROLE)
|
|
941
|
+
whenNotPaused
|
|
942
|
+
notBlacklisted(msg.sender)
|
|
943
|
+
{
|
|
944
|
+
_burn(msg.sender, amount);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ============ PAUSABLE ============
|
|
948
|
+
|
|
949
|
+
function pause() external onlyRole(PAUSER_ROLE) {
|
|
950
|
+
_pause();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function unpause() external onlyRole(PAUSER_ROLE) {
|
|
954
|
+
_unpause();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// ============ BLACKLIST ============
|
|
958
|
+
|
|
959
|
+
function blacklist(address account) external onlyRole(BLACKLISTER_ROLE) {
|
|
960
|
+
blacklisted[account] = true;
|
|
961
|
+
emit Blacklisted(account);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function unBlacklist(address account) external onlyRole(BLACKLISTER_ROLE) {
|
|
965
|
+
blacklisted[account] = false;
|
|
966
|
+
emit UnBlacklisted(account);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ============ TRANSFER OVERRIDES ============
|
|
970
|
+
|
|
971
|
+
function _update(address from, address to, uint256 value)
|
|
972
|
+
internal
|
|
973
|
+
override(ERC20, ERC20Pausable)
|
|
974
|
+
notBlacklisted(from)
|
|
975
|
+
notBlacklisted(to)
|
|
976
|
+
{
|
|
977
|
+
super._update(from, to, value);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
## Solidity vs OPNet Comparison
|
|
983
|
+
|
|
984
|
+
### Key Differences Table
|
|
985
|
+
|
|
986
|
+
| Aspect | Solidity (OpenZeppelin) | OPNet |
|
|
987
|
+
|--------|------------------------|-------|
|
|
988
|
+
| **Access Control** | `AccessControl` with `bytes32` role hashes | Bit flags in `u256` with enum |
|
|
989
|
+
| **Role Definition** | `keccak256("MINTER_ROLE")` | `enum Role { MINTER = 2 }` (powers of 2) |
|
|
990
|
+
| **Role Check** | `hasRole(MINTER_ROLE, account)` | `!SafeMath.and(roles, role).isZero()` |
|
|
991
|
+
| **Role Grant** | `_grantRole(role, account)` | `SafeMath.or(currentRoles, role)` |
|
|
992
|
+
| **Pausable** | `ERC20Pausable` extension | Manual `_paused: StoredBoolean` |
|
|
993
|
+
| **Modifiers** | `whenNotPaused`, `onlyRole()` | Inline function calls |
|
|
994
|
+
| **Blacklist** | `mapping(address => bool)` | `AddressMemoryMap` |
|
|
995
|
+
| **Multiple Inheritance** | `is ERC20, ERC20Pausable, AccessControl` | Single `extends OP20` |
|
|
996
|
+
| **Decimals** | Override `decimals()` function | Set in `OP20InitParameters` |
|
|
997
|
+
|
|
998
|
+
### Role System Comparison
|
|
999
|
+
|
|
1000
|
+
**Solidity (OpenZeppelin AccessControl):**
|
|
1001
|
+
```solidity
|
|
1002
|
+
// Roles as keccak256 hashes
|
|
1003
|
+
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
|
1004
|
+
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
|
|
1005
|
+
|
|
1006
|
+
// Each role is a separate mapping
|
|
1007
|
+
mapping(bytes32 role => mapping(address account => bool)) private _roles;
|
|
1008
|
+
|
|
1009
|
+
// Check role
|
|
1010
|
+
function hasRole(bytes32 role, address account) public view returns (bool) {
|
|
1011
|
+
return _roles[role][account];
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Grant role
|
|
1015
|
+
function grantRole(bytes32 role, address account) public onlyRole(getRoleAdmin(role)) {
|
|
1016
|
+
_grantRole(role, account);
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**OPNet (Bit Flag System):**
|
|
1021
|
+
```typescript
|
|
1022
|
+
// Roles as bit flags (powers of 2)
|
|
1023
|
+
enum Role {
|
|
1024
|
+
ADMIN = 1, // 0001
|
|
1025
|
+
MINTER = 2, // 0010
|
|
1026
|
+
PAUSER = 4, // 0100
|
|
1027
|
+
BLACKLISTER = 8 // 1000
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// All roles stored in single u256 per address
|
|
1031
|
+
private _roles: AddressMemoryMap; // address -> u256 (combined roles)
|
|
1032
|
+
|
|
1033
|
+
// Check role using bitwise AND
|
|
1034
|
+
public hasRole(account: Address, role: u256): bool {
|
|
1035
|
+
const roles = this._roles.get(account);
|
|
1036
|
+
return !SafeMath.and(roles, role).isZero();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Grant role using bitwise OR
|
|
1040
|
+
private _grantRole(account: Address, role: u256): void {
|
|
1041
|
+
const currentRoles = this._roles.get(account);
|
|
1042
|
+
const newRoles = SafeMath.or(currentRoles, role);
|
|
1043
|
+
this._roles.set(account, newRoles);
|
|
1044
|
+
}
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### Pausable Pattern Comparison
|
|
1048
|
+
|
|
1049
|
+
**Solidity (OpenZeppelin ERC20Pausable):**
|
|
1050
|
+
```solidity
|
|
1051
|
+
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
|
|
1052
|
+
|
|
1053
|
+
contract MyToken is ERC20Pausable {
|
|
1054
|
+
function pause() external onlyRole(PAUSER_ROLE) {
|
|
1055
|
+
_pause(); // Built-in from Pausable
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function unpause() external onlyRole(PAUSER_ROLE) {
|
|
1059
|
+
_unpause(); // Built-in from Pausable
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Transfers automatically checked via _update override
|
|
1063
|
+
}
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
**OPNet (Manual Implementation):**
|
|
1067
|
+
```typescript
|
|
1068
|
+
private _paused: StoredBoolean;
|
|
1069
|
+
|
|
1070
|
+
private whenNotPaused(): void {
|
|
1071
|
+
if (this._paused.value) {
|
|
1072
|
+
throw new Revert('Pausable: paused');
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
@method()
|
|
1077
|
+
@emit('Paused')
|
|
1078
|
+
public pause(_calldata: Calldata): BytesWriter {
|
|
1079
|
+
this.onlyRole(u256.fromU64(Role.PAUSER));
|
|
1080
|
+
this._paused.value = true;
|
|
1081
|
+
this.emitEvent(new Paused(Blockchain.tx.sender));
|
|
1082
|
+
return new BytesWriter(0);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Must manually call whenNotPaused() in each method
|
|
1086
|
+
public override transfer(calldata: Calldata): BytesWriter {
|
|
1087
|
+
this.whenNotPaused();
|
|
1088
|
+
// ... rest of transfer logic
|
|
1089
|
+
}
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Blacklist Pattern Comparison
|
|
1093
|
+
|
|
1094
|
+
**Solidity:**
|
|
1095
|
+
```solidity
|
|
1096
|
+
mapping(address => bool) public blacklisted;
|
|
1097
|
+
|
|
1098
|
+
modifier notBlacklisted(address account) {
|
|
1099
|
+
require(!blacklisted[account], "Blacklisted");
|
|
1100
|
+
_;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function blacklist(address account) external onlyRole(BLACKLISTER_ROLE) {
|
|
1104
|
+
blacklisted[account] = true;
|
|
1105
|
+
emit Blacklisted(account);
|
|
1106
|
+
}
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
**OPNet:**
|
|
1110
|
+
```typescript
|
|
1111
|
+
private _blacklist: AddressMemoryMap;
|
|
1112
|
+
|
|
1113
|
+
private notBlacklisted(account: Address): void {
|
|
1114
|
+
// AddressMemoryMap.get() returns u256; non-zero means blacklisted
|
|
1115
|
+
if (!this._blacklist.get(account).isZero()) {
|
|
1116
|
+
throw new Revert('Blacklisted');
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
@method({ name: 'account', type: ABIDataTypes.ADDRESS })
|
|
1121
|
+
@emit('Blacklisted')
|
|
1122
|
+
public blacklist(calldata: Calldata): BytesWriter {
|
|
1123
|
+
this.onlyRole(u256.fromU64(Role.BLACKLISTER));
|
|
1124
|
+
const account = calldata.readAddress();
|
|
1125
|
+
// AddressMemoryMap stores u256; use u256.One for true
|
|
1126
|
+
this._blacklist.set(account, u256.One);
|
|
1127
|
+
this.emitEvent(new Blacklisted(account));
|
|
1128
|
+
return new BytesWriter(0);
|
|
1129
|
+
}
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
### Advantages of OPNet Approach
|
|
1133
|
+
|
|
1134
|
+
| Feature | Benefit |
|
|
1135
|
+
|---------|---------|
|
|
1136
|
+
| **Efficient Role Storage** | Single u256 per address stores all roles efficiently |
|
|
1137
|
+
| **No Role Admin Complexity** | Simpler role hierarchy without OpenZeppelin's role admin system |
|
|
1138
|
+
| **Explicit Control Flow** | Manual checks make security-critical code paths visible |
|
|
1139
|
+
| **Bitcoin Security** | Inherits Bitcoin's proven consensus and security model |
|
|
1140
|
+
| **No Diamond Problem** | Single inheritance avoids Solidity's multiple inheritance issues |
|
|
1141
|
+
| **Custom Minter Allowance** | Built-in per-minter supply caps (like USDC) |
|
|
1142
|
+
|
|
1143
|
+
### Minter Allowance Pattern (USDC-style)
|
|
1144
|
+
|
|
1145
|
+
Both implementations support minter allowances, but OPNet makes this a first-class feature:
|
|
1146
|
+
|
|
1147
|
+
**Solidity:**
|
|
1148
|
+
```solidity
|
|
1149
|
+
mapping(address => uint256) public minterAllowance;
|
|
1150
|
+
|
|
1151
|
+
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
|
|
1152
|
+
require(minterAllowance[msg.sender] >= amount, "Minter allowance exceeded");
|
|
1153
|
+
minterAllowance[msg.sender] -= amount;
|
|
1154
|
+
_mint(to, amount);
|
|
1155
|
+
}
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
**OPNet:**
|
|
1159
|
+
```typescript
|
|
1160
|
+
private _minterAllowance: AddressMemoryMap;
|
|
1161
|
+
|
|
1162
|
+
@method(...)
|
|
1163
|
+
public mint(calldata: Calldata): BytesWriter {
|
|
1164
|
+
this.onlyRole(u256.fromU64(Role.MINTER));
|
|
1165
|
+
// ... validation
|
|
1166
|
+
const allowance = this._minterAllowance.get(minter);
|
|
1167
|
+
if (allowance < amount) {
|
|
1168
|
+
throw new Revert('Minter allowance exceeded');
|
|
1169
|
+
}
|
|
1170
|
+
this._minterAllowance.set(minter, SafeMath.sub(allowance, amount));
|
|
1171
|
+
this._mint(to, amount);
|
|
1172
|
+
return new BytesWriter(0);
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
---
|
|
1177
|
+
|
|
1178
|
+
**Navigation:**
|
|
1179
|
+
- Previous: [NFT with Reservations](./nft-with-reservations.md)
|
|
1180
|
+
- Next: [Oracle Integration](./oracle-integration.md)
|