@btc-vision/btc-runtime 1.10.10 → 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 +51 -26
- package/runtime/memory/MapOfMap.ts +1 -0
- package/LICENSE.md +0 -21
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
# OP20 Token Standard
|
|
2
|
+
|
|
3
|
+
OP20 is OPNet's fungible token standard, equivalent to Ethereum's ERC20. It provides a complete implementation for creating tokens with transfer, approval, and balance tracking capabilities.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
9
|
+
import {
|
|
10
|
+
OP20,
|
|
11
|
+
OP20InitParameters,
|
|
12
|
+
Blockchain,
|
|
13
|
+
Calldata,
|
|
14
|
+
BytesWriter,
|
|
15
|
+
ABIDataTypes,
|
|
16
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
export class MyToken extends OP20 {
|
|
20
|
+
public constructor() {
|
|
21
|
+
super();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public override onDeployment(_calldata: Calldata): void {
|
|
25
|
+
this.instantiate(new OP20InitParameters(
|
|
26
|
+
u256.fromString('1000000000000000000000000'), // maxSupply: 1M tokens
|
|
27
|
+
18, // decimals
|
|
28
|
+
'MyToken', // name
|
|
29
|
+
'MTK', // symbol
|
|
30
|
+
'https://example.com/icon.png' // icon (optional)
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// Mint initial supply to deployer
|
|
34
|
+
this._mint(Blockchain.tx.origin, this._maxSupply.value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ERC20 vs OP20 Comparison
|
|
40
|
+
|
|
41
|
+
| Feature | ERC20 (Solidity) | OP20 (OPNet) |
|
|
42
|
+
|---------|------------------|--------------|
|
|
43
|
+
| Language | Solidity | AssemblyScript |
|
|
44
|
+
| Runtime | EVM | WASM |
|
|
45
|
+
| Integer Type | `uint256` | `u256` |
|
|
46
|
+
| Max Decimals | 18 (convention) | 32 (hard limit) |
|
|
47
|
+
| Max Supply | Unlimited | Enforced at instantiation |
|
|
48
|
+
| Approval Pattern | `approve()` + `transferFrom()` | `increaseAllowance()`/`decreaseAllowance()` + `transferFrom()` |
|
|
49
|
+
| Unlimited Approval | Decremented on transfer | Optimized - not decremented |
|
|
50
|
+
| Events | Solidity events | `emitEvent()` system |
|
|
51
|
+
| Inheritance | Multiple inheritance | Single inheritance |
|
|
52
|
+
|
|
53
|
+
## Initialization
|
|
54
|
+
|
|
55
|
+
### OP20InitParameters
|
|
56
|
+
|
|
57
|
+
| Parameter | Type | Description |
|
|
58
|
+
|-----------|------|-------------|
|
|
59
|
+
| `maxSupply` | `u256` | Maximum token supply (cannot be exceeded) |
|
|
60
|
+
| `decimals` | `u8` | Decimal places (max 32) |
|
|
61
|
+
| `name` | `string` | Token name |
|
|
62
|
+
| `symbol` | `string` | Token symbol |
|
|
63
|
+
| `icon` | `string` | Token icon URL (optional, defaults to empty string) |
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const params = new OP20InitParameters(
|
|
67
|
+
u256.fromString('1000000000000000000000000000'), // 1 billion with 18 decimals
|
|
68
|
+
18,
|
|
69
|
+
'My Token',
|
|
70
|
+
'MTK',
|
|
71
|
+
'https://example.com/icon.png' // optional
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
this.instantiate(params);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Decimal Limit
|
|
78
|
+
|
|
79
|
+
**IMPORTANT:** Decimals cannot exceed 32.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Valid
|
|
83
|
+
const params = new OP20InitParameters(maxSupply, 18, name, symbol);
|
|
84
|
+
|
|
85
|
+
// Invalid - will throw
|
|
86
|
+
const params = new OP20InitParameters(maxSupply, 33, name, symbol);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Transfer Flow
|
|
90
|
+
|
|
91
|
+
The following diagram illustrates how a token transfer is processed:
|
|
92
|
+
|
|
93
|
+
```mermaid
|
|
94
|
+
---
|
|
95
|
+
config:
|
|
96
|
+
theme: dark
|
|
97
|
+
---
|
|
98
|
+
flowchart LR
|
|
99
|
+
A[👤 User signs TX] --> B[Submit to blockchain]
|
|
100
|
+
B --> C[Contract.transfer called]
|
|
101
|
+
C --> D{Valid recipient?}
|
|
102
|
+
D -->|No| E[Revert]
|
|
103
|
+
D -->|Yes| F{Sufficient balance?}
|
|
104
|
+
F -->|No| G[Revert]
|
|
105
|
+
F -->|Yes| H[Subtract from sender]
|
|
106
|
+
H --> I[Add to recipient]
|
|
107
|
+
I --> J[Emit TransferredEvent]
|
|
108
|
+
J --> K[Return success]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Detailed Transfer Sequence
|
|
112
|
+
|
|
113
|
+
```mermaid
|
|
114
|
+
sequenceDiagram
|
|
115
|
+
participant User as 👤 User Wallet
|
|
116
|
+
participant Blockchain as Bitcoin L1
|
|
117
|
+
participant VM as WASM Runtime
|
|
118
|
+
participant OP20 as OP20 Contract
|
|
119
|
+
participant Calldata as Calldata Reader
|
|
120
|
+
participant Storage as Storage Pointers
|
|
121
|
+
participant BalanceMap as balanceOfMap<br/>(Pointer 5)
|
|
122
|
+
participant TotalSupply as _totalSupply<br/>(Pointer 4)
|
|
123
|
+
participant EventLog as Event Log System
|
|
124
|
+
|
|
125
|
+
User->>Blockchain: Submit transfer(to, amount) TX
|
|
126
|
+
Blockchain->>VM: Execute transaction
|
|
127
|
+
VM->>OP20: Call transfer method
|
|
128
|
+
|
|
129
|
+
activate OP20
|
|
130
|
+
|
|
131
|
+
OP20->>Calldata: readAddress()
|
|
132
|
+
Calldata-->>OP20: to address
|
|
133
|
+
OP20->>Calldata: readU256()
|
|
134
|
+
Calldata-->>OP20: amount
|
|
135
|
+
|
|
136
|
+
OP20->>OP20: Get sender = Blockchain.tx.sender
|
|
137
|
+
Note over OP20: sender is msg.sender equivalent
|
|
138
|
+
|
|
139
|
+
OP20->>OP20: Validate to != Address.zero()
|
|
140
|
+
|
|
141
|
+
alt to is zero address
|
|
142
|
+
OP20->>VM: Revert('Cannot transfer to zero address')
|
|
143
|
+
VM->>User: Transaction failed
|
|
144
|
+
else Valid recipient
|
|
145
|
+
OP20->>OP20: _transfer(sender, to, amount)
|
|
146
|
+
|
|
147
|
+
OP20->>BalanceMap: get(sender)
|
|
148
|
+
BalanceMap->>Storage: Read from storage slot
|
|
149
|
+
Storage-->>BalanceMap: Raw balance data
|
|
150
|
+
BalanceMap-->>OP20: senderBalance: u256
|
|
151
|
+
|
|
152
|
+
alt Insufficient balance
|
|
153
|
+
OP20->>VM: Revert('Insufficient balance')
|
|
154
|
+
VM->>User: Transaction failed
|
|
155
|
+
else Sufficient balance
|
|
156
|
+
OP20->>OP20: SafeMath.sub(senderBalance, amount)
|
|
157
|
+
Note over OP20: Underflow protection
|
|
158
|
+
|
|
159
|
+
OP20->>BalanceMap: set(sender, newSenderBalance)
|
|
160
|
+
BalanceMap->>Storage: Write to storage slot
|
|
161
|
+
Note over Storage: Persistent state change
|
|
162
|
+
|
|
163
|
+
OP20->>BalanceMap: get(to)
|
|
164
|
+
BalanceMap->>Storage: Read recipient balance
|
|
165
|
+
Storage-->>BalanceMap: Recipient balance
|
|
166
|
+
BalanceMap-->>OP20: recipientBalance: u256
|
|
167
|
+
|
|
168
|
+
OP20->>OP20: SafeMath.add(recipientBalance, amount)
|
|
169
|
+
Note over OP20: Overflow protection
|
|
170
|
+
|
|
171
|
+
OP20->>BalanceMap: set(to, newRecipientBalance)
|
|
172
|
+
BalanceMap->>Storage: Write updated balance
|
|
173
|
+
Note over Storage: Both balances now updated
|
|
174
|
+
|
|
175
|
+
OP20->>OP20: Create TransferredEvent(operator, from, to, amount)
|
|
176
|
+
OP20->>EventLog: emitEvent(transferEvent)
|
|
177
|
+
Note over EventLog: Indexed for off-chain queries
|
|
178
|
+
|
|
179
|
+
OP20->>VM: Return BytesWriter(0)
|
|
180
|
+
deactivate OP20
|
|
181
|
+
|
|
182
|
+
VM->>Blockchain: Commit state changes
|
|
183
|
+
Blockchain->>User: Transaction success + receipt
|
|
184
|
+
Note over User: Balance updated,<br/>event emitted
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Token Lifecycle
|
|
190
|
+
|
|
191
|
+
```mermaid
|
|
192
|
+
stateDiagram-v2
|
|
193
|
+
[*] --> Undeployed
|
|
194
|
+
Undeployed --> Deployed: onDeployment(params)
|
|
195
|
+
|
|
196
|
+
state Deployed {
|
|
197
|
+
[*] --> ZeroSupply
|
|
198
|
+
ZeroSupply --> HasSupply: _mint()
|
|
199
|
+
HasSupply --> HasSupply: transfer()
|
|
200
|
+
HasSupply --> HasSupply: increaseAllowance()
|
|
201
|
+
HasSupply --> HasSupply: transferFrom()
|
|
202
|
+
HasSupply --> LowerSupply: _burn()
|
|
203
|
+
LowerSupply --> HasSupply: _mint()
|
|
204
|
+
LowerSupply --> ZeroSupply: _burn() all
|
|
205
|
+
|
|
206
|
+
state "Total Supply Management" as Supply {
|
|
207
|
+
[*] --> BelowMax
|
|
208
|
+
BelowMax --> BelowMax: _mint() within limit
|
|
209
|
+
BelowMax --> AtMax: _mint() to maxSupply
|
|
210
|
+
AtMax --> BelowMax: _burn()
|
|
211
|
+
BelowMax --> [*]: totalSupply = 0
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Built-in Methods
|
|
217
|
+
|
|
218
|
+
OP20 provides these methods automatically:
|
|
219
|
+
|
|
220
|
+
### Query Methods
|
|
221
|
+
|
|
222
|
+
| Method | Returns | Description |
|
|
223
|
+
|--------|---------|-------------|
|
|
224
|
+
| `name()` | `string` | Token name |
|
|
225
|
+
| `symbol()` | `string` | Token symbol |
|
|
226
|
+
| `icon()` | `string` | Token icon URL |
|
|
227
|
+
| `decimals()` | `u8` | Decimal places |
|
|
228
|
+
| `totalSupply()` | `u256` | Current total supply |
|
|
229
|
+
| `maximumSupply()` | `u256` | Maximum possible supply |
|
|
230
|
+
| `balanceOf(owner)` | `u256` | Balance of address |
|
|
231
|
+
| `allowance(owner, spender)` | `u256` | Approved amount |
|
|
232
|
+
| `nonceOf(owner)` | `u256` | Nonce for signature verification |
|
|
233
|
+
| `domainSeparator()` | `bytes32` | EIP-712 domain separator |
|
|
234
|
+
| `metadata()` | `multiple` | All token metadata in one call |
|
|
235
|
+
|
|
236
|
+
### Transfer Methods
|
|
237
|
+
|
|
238
|
+
| Method | Description |
|
|
239
|
+
|--------|-------------|
|
|
240
|
+
| `transfer(to, amount)` | Transfer tokens from sender |
|
|
241
|
+
| `transferFrom(from, to, amount)` | Transfer using approval |
|
|
242
|
+
| `safeTransfer(to, amount, data)` | Transfer with recipient callback |
|
|
243
|
+
| `safeTransferFrom(from, to, amount, data)` | TransferFrom with recipient callback |
|
|
244
|
+
|
|
245
|
+
### Approval Methods
|
|
246
|
+
|
|
247
|
+
| Method | Description |
|
|
248
|
+
|--------|-------------|
|
|
249
|
+
| `increaseAllowance(spender, amount)` | Increase approval |
|
|
250
|
+
| `decreaseAllowance(spender, amount)` | Decrease approval |
|
|
251
|
+
| `increaseAllowanceBySignature(...)` | Gasless approval increase via signature |
|
|
252
|
+
| `decreaseAllowanceBySignature(...)` | Gasless approval decrease via signature |
|
|
253
|
+
|
|
254
|
+
### Other Methods
|
|
255
|
+
|
|
256
|
+
| Method | Description |
|
|
257
|
+
|--------|-------------|
|
|
258
|
+
| `burn(amount)` | Burn tokens from sender's balance |
|
|
259
|
+
|
|
260
|
+
## Approval Flow
|
|
261
|
+
|
|
262
|
+
The following diagram shows how the approval and transferFrom pattern works:
|
|
263
|
+
|
|
264
|
+
```mermaid
|
|
265
|
+
---
|
|
266
|
+
config:
|
|
267
|
+
theme: dark
|
|
268
|
+
---
|
|
269
|
+
flowchart LR
|
|
270
|
+
A[👤 User increases allowance] --> B[Set allowance in storage]
|
|
271
|
+
B --> C[Emit ApprovedEvent]
|
|
272
|
+
C --> D[Spender calls transferFrom]
|
|
273
|
+
D --> E{Sufficient allowance?}
|
|
274
|
+
E -->|No| F[Revert]
|
|
275
|
+
E -->|Yes| G{Unlimited approval?}
|
|
276
|
+
G -->|Yes| H[Skip allowance update]
|
|
277
|
+
G -->|No| I[Decrease allowance]
|
|
278
|
+
H --> J[Execute transfer]
|
|
279
|
+
I --> J
|
|
280
|
+
J --> K[Update balances]
|
|
281
|
+
K --> L[Emit TransferredEvent]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Solidity Comparison
|
|
285
|
+
|
|
286
|
+
<table>
|
|
287
|
+
<tr>
|
|
288
|
+
<th>ERC20 (Solidity)</th>
|
|
289
|
+
<th>OP20 (OPNet)</th>
|
|
290
|
+
</tr>
|
|
291
|
+
<tr>
|
|
292
|
+
<td>
|
|
293
|
+
|
|
294
|
+
```solidity
|
|
295
|
+
contract MyToken is ERC20 {
|
|
296
|
+
constructor()
|
|
297
|
+
ERC20("MyToken", "MTK")
|
|
298
|
+
{
|
|
299
|
+
_mint(msg.sender, 1000000 * 10**18);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
</td>
|
|
305
|
+
<td>
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
@final
|
|
309
|
+
export class MyToken extends OP20 {
|
|
310
|
+
constructor() {
|
|
311
|
+
super();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
public override onDeployment(_: Calldata): void {
|
|
315
|
+
this.instantiate(new OP20InitParameters(
|
|
316
|
+
u256.fromString('1000000000000000000000000'),
|
|
317
|
+
18, 'MyToken', 'MTK', ''
|
|
318
|
+
));
|
|
319
|
+
this._mint(Blockchain.tx.origin, this._maxSupply.value);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
</td>
|
|
325
|
+
</tr>
|
|
326
|
+
</table>
|
|
327
|
+
|
|
328
|
+
## Storage Layout
|
|
329
|
+
|
|
330
|
+
OP20 uses the following storage pointers internally:
|
|
331
|
+
|
|
332
|
+
| Pointer | Storage | Description |
|
|
333
|
+
|---------|---------|-------------|
|
|
334
|
+
| 0 | `nonceMap` | Address -> nonce mapping (for signatures) |
|
|
335
|
+
| 1 | `maxSupply` | Maximum token supply |
|
|
336
|
+
| 2 | `decimals` | Decimal places |
|
|
337
|
+
| 3 | `stringPointer` | Shared pointer for name (sub 0), symbol (sub 1), icon (sub 2) |
|
|
338
|
+
| 4 | `totalSupply` | Current total supply |
|
|
339
|
+
| 5 | `allowanceMap` | Owner -> spender -> amount mapping |
|
|
340
|
+
| 6 | `balanceOfMap` | Address -> balance mapping |
|
|
341
|
+
|
|
342
|
+
**Note:** Your contract's pointers start after OP20's internal pointers (pointer 7+).
|
|
343
|
+
|
|
344
|
+
## Extending OP20
|
|
345
|
+
|
|
346
|
+
### Adding Custom Methods
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
@final
|
|
350
|
+
export class MyToken extends OP20 {
|
|
351
|
+
public constructor() {
|
|
352
|
+
super();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public override onDeployment(calldata: Calldata): void {
|
|
356
|
+
this.instantiate(new OP20InitParameters(
|
|
357
|
+
u256.fromString('1000000000000000000000000'),
|
|
358
|
+
18, 'MyToken', 'MTK'
|
|
359
|
+
));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Custom mint function (OP20 does not have a built-in public mint)
|
|
363
|
+
@method(
|
|
364
|
+
{ name: 'to', type: ABIDataTypes.ADDRESS },
|
|
365
|
+
{ name: 'amount', type: ABIDataTypes.UINT256 },
|
|
366
|
+
)
|
|
367
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
368
|
+
@emit('Minted')
|
|
369
|
+
public mint(calldata: Calldata): BytesWriter {
|
|
370
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
371
|
+
|
|
372
|
+
const to = calldata.readAddress();
|
|
373
|
+
const amount = calldata.readU256();
|
|
374
|
+
|
|
375
|
+
this._mint(to, amount);
|
|
376
|
+
// Note: _mint() already emits MintedEvent internally
|
|
377
|
+
|
|
378
|
+
return new BytesWriter(0);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Note: OP20 already has a public burn(amount) method built-in
|
|
382
|
+
// You can override it if you need custom behavior:
|
|
383
|
+
// public override burn(calldata: Calldata): BytesWriter { ... }
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Internal Methods
|
|
388
|
+
|
|
389
|
+
OP20 provides protected methods for extending functionality:
|
|
390
|
+
|
|
391
|
+
| Method | Description |
|
|
392
|
+
|--------|-------------|
|
|
393
|
+
| `_mint(to, amount)` | Mint new tokens |
|
|
394
|
+
| `_burn(from, amount)` | Burn tokens |
|
|
395
|
+
| `_transfer(from, to, amount)` | Internal transfer |
|
|
396
|
+
| `_balanceOf(owner)` | Get balance of address |
|
|
397
|
+
| `_allowance(owner, spender)` | Get allowance amount |
|
|
398
|
+
| `_increaseAllowance(owner, spender, amount)` | Increase allowance with overflow protection |
|
|
399
|
+
| `_decreaseAllowance(owner, spender, amount)` | Decrease allowance with underflow protection |
|
|
400
|
+
| `_spendAllowance(owner, spender, amount)` | Spend from allowance (for transferFrom) |
|
|
401
|
+
| `_safeTransfer(from, to, amount, data)` | Transfer with receiver callback |
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Minting tokens
|
|
405
|
+
this._mint(recipient, amount);
|
|
406
|
+
|
|
407
|
+
// Burning tokens
|
|
408
|
+
this._burn(holder, amount);
|
|
409
|
+
|
|
410
|
+
// Internal transfer (no sender checks)
|
|
411
|
+
this._transfer(from, to, amount);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Events
|
|
415
|
+
|
|
416
|
+
OP20 emits these events automatically:
|
|
417
|
+
|
|
418
|
+
### TransferredEvent
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// Emitted on transfer(), transferFrom(), safeTransfer(), safeTransferFrom()
|
|
422
|
+
TransferredEvent(operator: Address, from: Address, to: Address, amount: u256)
|
|
423
|
+
|
|
424
|
+
// operator: the address that initiated the transfer (Blockchain.tx.sender)
|
|
425
|
+
// from: the address tokens are transferred from
|
|
426
|
+
// to: the address tokens are transferred to
|
|
427
|
+
// amount: the number of tokens transferred
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### ApprovedEvent
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
// Emitted on increaseAllowance(), decreaseAllowance()
|
|
434
|
+
ApprovedEvent(owner: Address, spender: Address, amount: u256)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### MintedEvent
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// Emitted when new tokens are minted via _mint()
|
|
441
|
+
MintedEvent(to: Address, amount: u256)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### BurnedEvent
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Emitted when tokens are burned via burn() or _burn()
|
|
448
|
+
BurnedEvent(from: Address, amount: u256)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Approval Patterns
|
|
452
|
+
|
|
453
|
+
The following state diagram shows how an allowance transitions between different states:
|
|
454
|
+
|
|
455
|
+
```mermaid
|
|
456
|
+
---
|
|
457
|
+
config:
|
|
458
|
+
theme: dark
|
|
459
|
+
---
|
|
460
|
+
stateDiagram-v2
|
|
461
|
+
[*] --> NoAllowance
|
|
462
|
+
|
|
463
|
+
NoAllowance --> LimitedAllowance: increaseAllowance(amount)
|
|
464
|
+
NoAllowance --> UnlimitedAllowance: increaseAllowance(u256.Max)
|
|
465
|
+
|
|
466
|
+
LimitedAllowance --> LimitedAllowance: increaseAllowance(delta)
|
|
467
|
+
LimitedAllowance --> LimitedAllowance: decreaseAllowance(delta)
|
|
468
|
+
LimitedAllowance --> LimitedAllowance: transferFrom (decrements)
|
|
469
|
+
LimitedAllowance --> NoAllowance: transferFrom (exhausted)
|
|
470
|
+
LimitedAllowance --> NoAllowance: decreaseAllowance(all)
|
|
471
|
+
LimitedAllowance --> UnlimitedAllowance: increaseAllowance (overflow)
|
|
472
|
+
|
|
473
|
+
UnlimitedAllowance --> UnlimitedAllowance: transferFrom (no change)
|
|
474
|
+
UnlimitedAllowance --> LimitedAllowance: decreaseAllowance(amount)
|
|
475
|
+
UnlimitedAllowance --> NoAllowance: decreaseAllowance(all)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Standard Approval
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// User increases allowance for spender
|
|
482
|
+
increaseAllowance(spender, 1000);
|
|
483
|
+
|
|
484
|
+
// Spender can transfer up to 1000 tokens
|
|
485
|
+
transferFrom(user, recipient, 500); // Allowance now 500
|
|
486
|
+
transferFrom(user, recipient, 500); // Allowance now 0
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Unlimited Approval
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// Increase allowance to maximum - overflows to u256.Max
|
|
493
|
+
increaseAllowance(spender, u256.Max);
|
|
494
|
+
|
|
495
|
+
// Transfers don't reduce unlimited allowance
|
|
496
|
+
transferFrom(user, recipient, 1000); // Allowance still u256.Max
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Note:** OP20 optimizes unlimited approvals (u256.Max) - they're not decremented on transfer.
|
|
500
|
+
|
|
501
|
+
### Increase/Decrease Pattern
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// Safe pattern using increase/decrease (prevents front-running)
|
|
505
|
+
increaseAllowance(spender, 100); // Add 100 to current allowance
|
|
506
|
+
decreaseAllowance(spender, 50); // Remove 50 from current allowance
|
|
507
|
+
|
|
508
|
+
// Note: If decrease amount > current allowance, it sets to zero (no underflow)
|
|
509
|
+
// If increase would overflow, it sets to u256.Max (unlimited)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Edge Cases
|
|
513
|
+
|
|
514
|
+
The following state diagram shows how token balances transition for an individual address:
|
|
515
|
+
|
|
516
|
+
```mermaid
|
|
517
|
+
---
|
|
518
|
+
config:
|
|
519
|
+
theme: dark
|
|
520
|
+
---
|
|
521
|
+
stateDiagram-v2
|
|
522
|
+
[*] --> ZeroBalance
|
|
523
|
+
|
|
524
|
+
ZeroBalance --> HasBalance: receive tokens
|
|
525
|
+
|
|
526
|
+
HasBalance --> HasBalance: transfer (partial)
|
|
527
|
+
HasBalance --> HasBalance: receive more
|
|
528
|
+
HasBalance --> ZeroBalance: transfer (all)
|
|
529
|
+
HasBalance --> ZeroBalance: burn (all)
|
|
530
|
+
|
|
531
|
+
note right of HasBalance
|
|
532
|
+
Balance can increase via:
|
|
533
|
+
- _mint()
|
|
534
|
+
- transfer()
|
|
535
|
+
- transferFrom()
|
|
536
|
+
end note
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Zero Address
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
// Transfer to zero address reverts
|
|
543
|
+
transfer(Address.zero(), amount); // Throws: "Cannot transfer to zero address"
|
|
544
|
+
|
|
545
|
+
// Minting to zero address reverts
|
|
546
|
+
_mint(Address.zero(), amount); // Throws
|
|
547
|
+
|
|
548
|
+
// Burning from zero address reverts
|
|
549
|
+
_burn(Address.zero(), amount); // Throws
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Overflow Protection
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
// Minting beyond maxSupply reverts
|
|
556
|
+
_mint(to, amount); // Throws if totalSupply + amount > maxSupply
|
|
557
|
+
|
|
558
|
+
// All arithmetic uses SafeMath
|
|
559
|
+
// Overflow/underflow automatically reverts
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Self-Approval
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
// Approving yourself is valid but pointless
|
|
566
|
+
increaseAllowance(Blockchain.tx.sender, amount); // Works, but why?
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## Complete Token Example
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
573
|
+
import {
|
|
574
|
+
OP20,
|
|
575
|
+
OP20InitParameters,
|
|
576
|
+
Blockchain,
|
|
577
|
+
Address,
|
|
578
|
+
Calldata,
|
|
579
|
+
BytesWriter,
|
|
580
|
+
Selector,
|
|
581
|
+
SafeMath,
|
|
582
|
+
Revert,
|
|
583
|
+
StoredBoolean,
|
|
584
|
+
AddressMemoryMap,
|
|
585
|
+
ABIDataTypes,
|
|
586
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
587
|
+
|
|
588
|
+
@final
|
|
589
|
+
export class AdvancedToken extends OP20 {
|
|
590
|
+
// Additional storage
|
|
591
|
+
private pausedPointer: u16 = Blockchain.nextPointer;
|
|
592
|
+
private blacklistPointer: u16 = Blockchain.nextPointer;
|
|
593
|
+
|
|
594
|
+
private _paused: StoredBoolean = new StoredBoolean(this.pausedPointer, false);
|
|
595
|
+
private _blacklist: AddressMemoryMap;
|
|
596
|
+
|
|
597
|
+
public constructor() {
|
|
598
|
+
super();
|
|
599
|
+
this._blacklist = new AddressMemoryMap(this.blacklistPointer);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
public override onDeployment(calldata: Calldata): void {
|
|
603
|
+
const maxSupply = calldata.readU256();
|
|
604
|
+
const decimals = calldata.readU8();
|
|
605
|
+
const name = calldata.readString();
|
|
606
|
+
const symbol = calldata.readString();
|
|
607
|
+
|
|
608
|
+
this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Override transfer to add checks
|
|
612
|
+
public override transfer(calldata: Calldata): BytesWriter {
|
|
613
|
+
this.whenNotPaused();
|
|
614
|
+
this.checkBlacklist(Blockchain.tx.sender);
|
|
615
|
+
|
|
616
|
+
return super.transfer(calldata);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Admin: Mint tokens
|
|
620
|
+
@method(
|
|
621
|
+
{ name: 'to', type: ABIDataTypes.ADDRESS },
|
|
622
|
+
{ name: 'amount', type: ABIDataTypes.UINT256 },
|
|
623
|
+
)
|
|
624
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
625
|
+
@emit('Minted')
|
|
626
|
+
public mint(calldata: Calldata): BytesWriter {
|
|
627
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
628
|
+
this._mint(calldata.readAddress(), calldata.readU256());
|
|
629
|
+
// Note: _mint() already emits MintedEvent internally
|
|
630
|
+
return new BytesWriter(0);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Admin: Pause/unpause
|
|
634
|
+
@method()
|
|
635
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
636
|
+
public pause(_calldata: Calldata): BytesWriter {
|
|
637
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
638
|
+
this._paused.value = true;
|
|
639
|
+
return new BytesWriter(0);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
@method()
|
|
643
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
644
|
+
public unpause(_calldata: Calldata): BytesWriter {
|
|
645
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
646
|
+
this._paused.value = false;
|
|
647
|
+
return new BytesWriter(0);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Admin: Blacklist management
|
|
651
|
+
@method({ name: 'address', type: ABIDataTypes.ADDRESS })
|
|
652
|
+
@returns({ name: 'success', type: ABIDataTypes.BOOL })
|
|
653
|
+
public blacklist(calldata: Calldata): BytesWriter {
|
|
654
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
655
|
+
this._blacklist.set(calldata.readAddress(), true);
|
|
656
|
+
return new BytesWriter(0);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Internal helpers
|
|
660
|
+
private whenNotPaused(): void {
|
|
661
|
+
if (this._paused.value) {
|
|
662
|
+
throw new Revert('Token is paused');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private checkBlacklist(address: Address): void {
|
|
667
|
+
if (this._blacklist.get(address)) {
|
|
668
|
+
throw new Revert('Address is blacklisted');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
## Best Practices
|
|
675
|
+
|
|
676
|
+
1. **Always call `instantiate()` in `onDeployment`**
|
|
677
|
+
2. **Use SafeMath for any custom arithmetic**
|
|
678
|
+
3. **Emit events for custom state changes**
|
|
679
|
+
4. **Validate all inputs before processing**
|
|
680
|
+
5. **Use `_mint`/`_burn` for supply changes**
|
|
681
|
+
6. **Override `transfer` carefully (call `super`)**
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
**Navigation:**
|
|
686
|
+
- Previous: [OP_NET Base](./op-net-base.md)
|
|
687
|
+
- Next: [OP20S Signatures](./op20s-signatures.md)
|