├── src
├── tsconfig.json
├── nft
│ ├── index.ts
│ ├── events
│ │ └── Reserved.ts
│ └── MyNFT.ts
├── token
│ ├── index.ts
│ └── MyToken.ts
├── stablecoin
│ ├── index.ts
│ ├── events
│ │ └── StableCoinEvents.ts
│ └── MyStableCoin.ts
├── pegged-token
│ ├── index.ts
│ └── MyPeggedToken.ts
├── multi-oracle-stablecoin
│ ├── index.ts
│ └── MyMultiOracleStable.ts
└── shared-events
│ └── OracleEvents.ts
├── .prettierrc.json
├── .vscode
└── settings.json
├── .github
└── dependabot.yml
├── tsconfig.json
├── LICENSE
├── eslint.config.js
├── asconfig.json
├── package.json
├── .gitignore
├── .gitattributes
├── README.md
└── docs
└── OP_20.md
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@btc-vision/opnet-transform/std/assembly.json",
3 | "include": [
4 | "./**/*.ts"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "trailingComma": "all",
4 | "tabWidth": 4,
5 | "semi": true,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "bracketSpacing": true,
9 | "bracketSameLine": true,
10 | "arrowParens": "always",
11 | "singleAttributePerLine": true
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": ["javascript", "typescript"],
3 | "prettier.useEditorConfig": false,
4 | "prettier.useTabs": false,
5 | "prettier.configPath": ".prettierrc.json",
6 | "prettier.requireConfig": true,
7 | "prettier.tabWidth": 4,
8 | "prettier.singleQuote": true,
9 | "editor.formatOnSave": true,
10 | }
--------------------------------------------------------------------------------
/src/nft/index.ts:
--------------------------------------------------------------------------------
1 | import { Blockchain } from '@btc-vision/btc-runtime/runtime';
2 | import { revertOnError } from '@btc-vision/btc-runtime/runtime/abort/abort';
3 | import { MyNFT } from './MyNFT';
4 |
5 | // DO NOT TOUCH TO THIS.
6 | Blockchain.contract = () => {
7 | // ONLY CHANGE THE CONTRACT CLASS NAME.
8 | // DO NOT ADD CUSTOM LOGIC HERE.
9 |
10 | return new MyNFT();
11 | };
12 |
13 | // VERY IMPORTANT
14 | export * from '@btc-vision/btc-runtime/runtime/exports';
15 |
16 | // VERY IMPORTANT
17 | export function abort(message: string, fileName: string, line: u32, column: u32): void {
18 | revertOnError(message, fileName, line, column);
19 | }
20 |
--------------------------------------------------------------------------------
/src/token/index.ts:
--------------------------------------------------------------------------------
1 | import { Blockchain } from '@btc-vision/btc-runtime/runtime';
2 | import { revertOnError } from '@btc-vision/btc-runtime/runtime/abort/abort';
3 | import { MyToken } from './MyToken';
4 |
5 | // DO NOT TOUCH TO THIS.
6 | Blockchain.contract = () => {
7 | // ONLY CHANGE THE CONTRACT CLASS NAME.
8 | // DO NOT ADD CUSTOM LOGIC HERE.
9 |
10 | return new MyToken();
11 | };
12 |
13 | // VERY IMPORTANT
14 | export * from '@btc-vision/btc-runtime/runtime/exports';
15 |
16 | // VERY IMPORTANT
17 | export function abort(message: string, fileName: string, line: u32, column: u32): void {
18 | revertOnError(message, fileName, line, column);
19 | }
20 |
--------------------------------------------------------------------------------
/src/stablecoin/index.ts:
--------------------------------------------------------------------------------
1 | import { Blockchain } from '@btc-vision/btc-runtime/runtime';
2 | import { revertOnError } from '@btc-vision/btc-runtime/runtime/abort/abort';
3 | import { MyStableCoin } from './MyStableCoin';
4 |
5 | // DO NOT TOUCH TO THIS.
6 | Blockchain.contract = () => {
7 | // ONLY CHANGE THE CONTRACT CLASS NAME.
8 | // DO NOT ADD CUSTOM LOGIC HERE.
9 |
10 | return new MyStableCoin();
11 | };
12 |
13 | // VERY IMPORTANT
14 | export * from '@btc-vision/btc-runtime/runtime/exports';
15 |
16 | // VERY IMPORTANT
17 | export function abort(message: string, fileName: string, line: u32, column: u32): void {
18 | revertOnError(message, fileName, line, column);
19 | }
20 |
--------------------------------------------------------------------------------
/src/pegged-token/index.ts:
--------------------------------------------------------------------------------
1 | import { Blockchain } from '@btc-vision/btc-runtime/runtime';
2 | import { revertOnError } from '@btc-vision/btc-runtime/runtime/abort/abort';
3 | import { MyPeggedToken } from './MyPeggedToken';
4 |
5 | // DO NOT TOUCH TO THIS.
6 | Blockchain.contract = () => {
7 | // ONLY CHANGE THE CONTRACT CLASS NAME.
8 | // DO NOT ADD CUSTOM LOGIC HERE.
9 |
10 | return new MyPeggedToken();
11 | };
12 |
13 | // VERY IMPORTANT
14 | export * from '@btc-vision/btc-runtime/runtime/exports';
15 |
16 | // VERY IMPORTANT
17 | export function abort(message: string, fileName: string, line: u32, column: u32): void {
18 | revertOnError(message, fileName, line, column);
19 | }
20 |
--------------------------------------------------------------------------------
/src/multi-oracle-stablecoin/index.ts:
--------------------------------------------------------------------------------
1 | import { Blockchain } from '@btc-vision/btc-runtime/runtime';
2 | import { revertOnError } from '@btc-vision/btc-runtime/runtime/abort/abort';
3 | import { MultiOracleStablecoin } from './MyMultiOracleStable';
4 |
5 | // DO NOT TOUCH TO THIS.
6 | Blockchain.contract = () => {
7 | // ONLY CHANGE THE CONTRACT CLASS NAME.
8 | // DO NOT ADD CUSTOM LOGIC HERE.
9 |
10 | return new MultiOracleStablecoin();
11 | };
12 |
13 | // VERY IMPORTANT
14 | export * from '@btc-vision/btc-runtime/runtime/exports';
15 |
16 | // VERY IMPORTANT
17 | export function abort(message: string, fileName: string, line: u32, column: u32): void {
18 | revertOnError(message, fileName, line, column);
19 | }
20 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: 'npm'
5 | directory: '/'
6 | schedule:
7 | interval: 'daily'
8 | time: '06:00'
9 | timezone: 'America/Toronto'
10 | open-pull-requests-limit: 10
11 | rebase-strategy: 'auto'
12 |
13 | groups:
14 | production-deps:
15 | dependency-type: 'production'
16 | update-types: ['minor', 'patch']
17 | dev-deps:
18 | dependency-type: 'development'
19 | update-types: ['minor', 'patch']
20 |
21 | commit-message:
22 | prefix: '⬆️'
23 | include: scope
24 |
25 | labels:
26 | - 'dependencies'
27 | - 'npm'
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "declaration": true,
5 | "target": "esnext",
6 | "noImplicitAny": true,
7 | "removeComments": true,
8 | "suppressImplicitAnyIndexErrors": false,
9 | "preserveConstEnums": true,
10 | "resolveJsonModule": true,
11 | "skipLibCheck": false,
12 | "sourceMap": false,
13 | "moduleDetection": "force",
14 | "experimentalDecorators": true,
15 | "lib": [
16 | "es6"
17 | ],
18 | "strict": true,
19 | "strictNullChecks": true,
20 | "strictFunctionTypes": true,
21 | "strictBindCallApply": true,
22 | "strictPropertyInitialization": true,
23 | "alwaysStrict": true,
24 | "moduleResolution": "node",
25 | "allowJs": false,
26 | "incremental": true,
27 | "allowSyntheticDefaultImports": true,
28 | "outDir": "build"
29 | },
30 | "include": [
31 | "./tests/*.ts",
32 | "./tests/**/*.ts",
33 | "./helper/*.ts",
34 | "./helper/**/*.ts"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 [ORANGE PILLS INC, 2140 S DUPONT HWY CAMDEN, DE 19934]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import tseslint from 'typescript-eslint';
4 | import eslint from '@eslint/js';
5 |
6 | export default tseslint.config(
7 | eslint.configs.recommended,
8 | ...tseslint.configs.strictTypeChecked,
9 | {
10 | languageOptions: {
11 | parserOptions: {
12 | projectService: true,
13 | tsconfigDirName: import.meta.dirname,
14 | },
15 | },
16 | rules: {
17 | 'no-undef': 'off',
18 | '@typescript-eslint/no-unused-vars': 'off',
19 | 'no-empty': 'off',
20 | '@typescript-eslint/restrict-template-expressions': 'off',
21 | '@typescript-eslint/only-throw-error': 'off',
22 | '@typescript-eslint/no-unnecessary-condition': 'off',
23 | '@typescript-eslint/unbound-method': 'warn',
24 | '@typescript-eslint/no-confusing-void-expression': 'off',
25 | '@typescript-eslint/no-extraneous-class': 'off',
26 | '@typescript-eslint/restrict-plus-operands': 'off',
27 | '@typescript-eslint/no-unnecessary-type-assertion': 'off',
28 | '@typescript-eslint/no-unsafe-call': 'off',
29 | },
30 | },
31 | {
32 | files: ['**/*.js'],
33 | ...tseslint.configs.disableTypeChecked,
34 | },
35 | );
36 |
--------------------------------------------------------------------------------
/asconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "token": {
4 | "outFile": "build/MyToken.wasm",
5 | "use": ["abort=src/token/index/abort"]
6 | },
7 | "stablecoin": {
8 | "outFile": "build/MyStableCoin.wasm",
9 | "use": ["abort=src/stablecoin/index/abort"]
10 | },
11 | "peggedcoin": {
12 | "outFile": "build/MyPeggedCoin.wasm",
13 | "use": ["abort=src/pegged-token/index/abort"]
14 | },
15 | "oraclecoin": {
16 | "outFile": "build/MyOracleCoin.wasm",
17 | "use": ["abort=src/multi-oracle-stablecoin/index/abort"]
18 | },
19 | "nft": {
20 | "outFile": "build/MyNFT.wasm",
21 | "use": ["abort=src/nft/index/abort"]
22 | }
23 | },
24 | "options": {
25 | "sourceMap": false,
26 | "optimizeLevel": 3,
27 | "shrinkLevel": 1,
28 | "converge": true,
29 | "noAssert": false,
30 | "enable": [
31 | "sign-extension",
32 | "mutable-globals",
33 | "nontrapping-f2i",
34 | "bulk-memory",
35 | "simd",
36 | "reference-types",
37 | "multi-value"
38 | ],
39 | "runtime": "stub",
40 | "memoryBase": 0,
41 | "initialMemory": 1,
42 | "exportStart": "start",
43 | "transform": "@btc-vision/opnet-transform"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@btc-vision/op20",
3 | "version": "0.0.1",
4 | "description": "OP_20 example smart contract",
5 | "main": "index.js",
6 | "scripts": {
7 | "build:token": "asc src/token/index.ts --target token --measure --uncheckedBehavior never",
8 | "build:stablecoin": "asc src/stablecoin/index.ts --target stablecoin --measure --uncheckedBehavior never",
9 | "build:peggedcoin": "asc src/pegged-token/index.ts --target peggedcoin --measure --uncheckedBehavior never",
10 | "build:oraclecoin": "asc src/multi-oracle-stablecoin/index.ts --target oraclecoin --measure --uncheckedBehavior never",
11 | "build:nft": "asc src/nft/index.ts --target nft --measure --uncheckedBehavior never"
12 | },
13 | "author": "BlobMaster41",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "@types/node": "^25.0.1",
17 | "assemblyscript": "^0.28.9",
18 | "prettier": "^3.7.4"
19 | },
20 | "keywords": [
21 | "bitcoin",
22 | "smart",
23 | "contract",
24 | "runtime",
25 | "opnet",
26 | "OP_NET",
27 | "wrapped bitcoin",
28 | "wbtc"
29 | ],
30 | "resolutions": {
31 | "@btc-vision/btc-runtime": "^1.10.8"
32 | },
33 | "homepage": "https://opnet.org",
34 | "type": "module",
35 | "dependencies": {
36 | "@assemblyscript/loader": "^0.28.9",
37 | "@btc-vision/as-bignum": "^0.0.7",
38 | "@btc-vision/btc-runtime": "^1.10.12",
39 | "@btc-vision/opnet-transform": "^0.2.1",
40 | "@eslint/js": "^9.39.1",
41 | "gulplog": "^2.2.0",
42 | "ts-node": "^10.9.2",
43 | "typescript": "^5.9.3",
44 | "typescript-eslint": "^8.49.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/nft/events/Reserved.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from '@btc-vision/as-bignum/assembly';
2 | import {
3 | Address,
4 | ADDRESS_BYTE_LENGTH,
5 | BOOLEAN_BYTE_LENGTH,
6 | BytesWriter,
7 | NetEvent,
8 | U256_BYTE_LENGTH,
9 | U64_BYTE_LENGTH,
10 | } from '@btc-vision/btc-runtime/runtime';
11 |
12 | @final
13 | export class ReservationCreatedEvent extends NetEvent {
14 | constructor(user: Address, amount: u256, block: u64, feePaid: u64) {
15 | const eventData: BytesWriter = new BytesWriter(
16 | ADDRESS_BYTE_LENGTH + U256_BYTE_LENGTH + U64_BYTE_LENGTH * 2,
17 | );
18 | eventData.writeAddress(user);
19 | eventData.writeU256(amount);
20 | eventData.writeU64(block);
21 | eventData.writeU64(feePaid);
22 |
23 | super('ReservationCreated', eventData);
24 | }
25 | }
26 |
27 | @final
28 | export class ReservationClaimedEvent extends NetEvent {
29 | constructor(user: Address, amount: u256, firstTokenId: u256) {
30 | const eventData: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH + U256_BYTE_LENGTH * 2);
31 | eventData.writeAddress(user);
32 | eventData.writeU256(amount);
33 | eventData.writeU256(firstTokenId);
34 |
35 | super('ReservationClaimed', eventData);
36 | }
37 | }
38 |
39 | @final
40 | export class ReservationExpiredEvent extends NetEvent {
41 | constructor(block: u64, amountRecovered: u256) {
42 | const eventData: BytesWriter = new BytesWriter(U64_BYTE_LENGTH + U256_BYTE_LENGTH);
43 | eventData.writeU64(block);
44 | eventData.writeU256(amountRecovered);
45 |
46 | super('ReservationExpired', eventData);
47 | }
48 | }
49 |
50 | @final
51 | export class MintStatusChangedEvent extends NetEvent {
52 | constructor(enabled: boolean) {
53 | const eventData = new BytesWriter(BOOLEAN_BYTE_LENGTH);
54 | eventData.writeBoolean(enabled);
55 |
56 | super('MintStatusChanged', eventData);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | package-lock.json
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional stylelint cache
57 | .stylelintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variable files
75 | .env
76 | .env.development.local
77 | .env.test.local
78 | .env.production.local
79 | .env.local
80 |
81 | # parcel-bundler cache (https://parceljs.org/)
82 | .cache
83 | .parcel-cache
84 |
85 | # Next.js build output
86 | .next
87 | out
88 |
89 | # Nuxt.js build / generate output
90 | .nuxt
91 | dist
92 |
93 | # Gatsby files
94 | .cache/
95 | # Comment in the public line in if your project uses Gatsby and not Next.js
96 | # https://nextjs.org/blog/next-9-1#public-directory-support
97 | # public
98 |
99 | # vuepress build output
100 | .vuepress/dist
101 |
102 | # vuepress v2.x temp and cache directory
103 | .temp
104 |
105 | # Docusaurus cache and generated files
106 | .docusaurus
107 |
108 | # Serverless directories
109 | .serverless/
110 |
111 | # FuseBox cache
112 | .fusebox/
113 |
114 | # DynamoDB Local files
115 | .dynamodb/
116 |
117 | # TernJS port file
118 | .tern-port
119 |
120 | # Stores VSCode versions used for testing VSCode extensions
121 | .vscode-test
122 |
123 | # yarn v2
124 | .yarn/cache
125 | .yarn/unplugged
126 | .yarn/build-state.yml
127 | .yarn/install-state.gz
128 | .pnp.*
129 |
130 | contract/V1ERC20/account-sepolia.txt
131 | tools/db/dump.rdb
132 |
133 | *.key
134 | *.wallet
135 |
136 | *.zip
137 |
138 | tools/db/dump.rdb
139 |
140 | fixdb/
141 | /.vs
142 | .idea
143 |
144 | build
145 | debug
146 | yarn.lock
147 | abis/
--------------------------------------------------------------------------------
/src/shared-events/OracleEvents.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from '@btc-vision/as-bignum/assembly';
2 | import {
3 | Address,
4 | ADDRESS_BYTE_LENGTH,
5 | BytesWriter,
6 | NetEvent,
7 | U256_BYTE_LENGTH,
8 | U32_BYTE_LENGTH,
9 | U64_BYTE_LENGTH,
10 | } from '@btc-vision/btc-runtime/runtime';
11 |
12 | @final
13 | export class OracleAddedEvent extends NetEvent {
14 | constructor(oracle: Address, addedBy: Address) {
15 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
16 | data.writeAddress(oracle);
17 | data.writeAddress(addedBy);
18 |
19 | super('OracleAdded', data);
20 | }
21 | }
22 |
23 | @final
24 | export class OracleRemovedEvent extends NetEvent {
25 | constructor(oracle: Address, removedBy: Address) {
26 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
27 | data.writeAddress(oracle);
28 | data.writeAddress(removedBy);
29 |
30 | super('OracleRemoved', data);
31 | }
32 | }
33 |
34 | @final
35 | export class PriceSubmittedEvent extends NetEvent {
36 | constructor(oracle: Address, price: u256, blockNumber: u64) {
37 | const data: BytesWriter = new BytesWriter(
38 | ADDRESS_BYTE_LENGTH + U256_BYTE_LENGTH + U64_BYTE_LENGTH,
39 | );
40 | data.writeAddress(oracle);
41 | data.writeU256(price);
42 | data.writeU64(blockNumber);
43 |
44 | super('PriceSubmitted', data);
45 | }
46 | }
47 |
48 | @final
49 | export class PriceAggregatedEvent extends NetEvent {
50 | constructor(medianPrice: u256, oracleCount: u32, blockNumber: u64) {
51 | const data: BytesWriter = new BytesWriter(
52 | U256_BYTE_LENGTH + U32_BYTE_LENGTH + U64_BYTE_LENGTH,
53 | );
54 | data.writeU256(medianPrice);
55 | data.writeU32(oracleCount);
56 | data.writeU64(blockNumber);
57 |
58 | super('PriceAggregated', data);
59 | }
60 | }
61 |
62 | @final
63 | export class TWAPUpdatedEvent extends NetEvent {
64 | constructor(oldPrice: u256, newPrice: u256, timeElapsed: u64) {
65 | const data: BytesWriter = new BytesWriter(U256_BYTE_LENGTH * 2 + U64_BYTE_LENGTH);
66 | data.writeU256(oldPrice);
67 | data.writeU256(newPrice);
68 | data.writeU64(timeElapsed);
69 |
70 | super('TWAPUpdated', data);
71 | }
72 | }
73 |
74 | @final
75 | export class PoolChangedEvent extends NetEvent {
76 | constructor(previousPool: Address, newPool: Address) {
77 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
78 | data.writeAddress(previousPool);
79 | data.writeAddress(newPool);
80 |
81 | super('PoolChanged', data);
82 | }
83 | }
84 |
85 | @final
86 | export class CustodianChangedEvent extends NetEvent {
87 | constructor(previousCustodian: Address, newCustodian: Address) {
88 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
89 | data.writeAddress(previousCustodian);
90 | data.writeAddress(newCustodian);
91 |
92 | super('CustodianChanged', data);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/token/MyToken.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from '@btc-vision/as-bignum/assembly';
2 | import {
3 | Address,
4 | AddressMap,
5 | Blockchain,
6 | BytesWriter,
7 | Calldata,
8 | OP20,
9 | OP20InitParameters,
10 | SafeMath,
11 | } from '@btc-vision/btc-runtime/runtime';
12 |
13 | @final
14 | export class MyToken extends OP20 {
15 | public constructor() {
16 | super();
17 |
18 | // IMPORTANT. THIS WILL RUN EVERYTIME THE CONTRACT IS INTERACTED WITH. FOR SPECIFIC INITIALIZATION, USE "onDeployment" METHOD.
19 | }
20 |
21 | // "solidityLikeConstructor" This is a solidity-like constructor. This method will only run once when the contract is deployed.
22 | public override onDeployment(_calldata: Calldata): void {
23 | const maxSupply: u256 = u256.fromString('1000000000000000000000000000'); // Your max supply. (Here, 1 billion tokens)
24 | const decimals: u8 = 18; // Your decimals.
25 | const name: string = 'Test'; // Your token name.
26 | const symbol: string = 'TEST'; // Your token symbol.
27 |
28 | this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
29 |
30 | // Add your logic here. Eg, minting the initial supply:
31 | // this._mint(Blockchain.tx.origin, maxSupply);
32 | }
33 |
34 | @method(
35 | {
36 | name: 'address',
37 | type: ABIDataTypes.ADDRESS,
38 | },
39 | {
40 | name: 'amount',
41 | type: ABIDataTypes.UINT256,
42 | },
43 | )
44 | @emit('Minted')
45 | public mint(calldata: Calldata): BytesWriter {
46 | this.onlyDeployer(Blockchain.tx.sender);
47 |
48 | this._mint(calldata.readAddress(), calldata.readU256());
49 |
50 | return new BytesWriter(0);
51 | }
52 |
53 | /**
54 | * Mints tokens to the specified addresses.
55 | *
56 | * @param calldata Calldata containing an `AddressMap
` to mint to.
57 | */
58 | @method({
59 | name: 'addressAndAmount',
60 | type: ABIDataTypes.ADDRESS_UINT256_TUPLE,
61 | })
62 | @emit('Minted')
63 | public airdrop(calldata: Calldata): BytesWriter {
64 | this.onlyDeployer(Blockchain.tx.sender);
65 |
66 | const addressAndAmount: AddressMap = calldata.readAddressMapU256();
67 | const addresses: Address[] = addressAndAmount.keys();
68 |
69 | let totalAirdropped: u256 = u256.Zero;
70 |
71 | for (let i: i32 = 0; i < addresses.length; i++) {
72 | const address = addresses[i];
73 | const amount = addressAndAmount.get(address);
74 |
75 | const currentBalance: u256 = this.balanceOfMap.get(address);
76 |
77 | if (currentBalance) {
78 | this.balanceOfMap.set(address, SafeMath.add(currentBalance, amount));
79 | } else {
80 | this.balanceOfMap.set(address, amount);
81 | }
82 |
83 | totalAirdropped = SafeMath.add(totalAirdropped, amount);
84 |
85 | this.createMintedEvent(address, amount);
86 | }
87 |
88 | this._totalSupply.set(SafeMath.add(this._totalSupply.value, totalAirdropped));
89 |
90 | return new BytesWriter(0);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/stablecoin/events/StableCoinEvents.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Address,
3 | ADDRESS_BYTE_LENGTH,
4 | BytesWriter,
5 | NetEvent,
6 | } from '@btc-vision/btc-runtime/runtime';
7 |
8 | @final
9 | export class BlacklistedEvent extends NetEvent {
10 | constructor(account: Address, blacklister: Address) {
11 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
12 | data.writeAddress(account);
13 | data.writeAddress(blacklister);
14 |
15 | super('Blacklisted', data);
16 | }
17 | }
18 |
19 | @final
20 | export class UnblacklistedEvent extends NetEvent {
21 | constructor(account: Address, blacklister: Address) {
22 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
23 | data.writeAddress(account);
24 | data.writeAddress(blacklister);
25 |
26 | super('Unblacklisted', data);
27 | }
28 | }
29 |
30 | @final
31 | export class PausedEvent extends NetEvent {
32 | constructor(pauser: Address) {
33 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH);
34 | data.writeAddress(pauser);
35 |
36 | super('Paused', data);
37 | }
38 | }
39 |
40 | @final
41 | export class UnpausedEvent extends NetEvent {
42 | constructor(pauser: Address) {
43 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH);
44 | data.writeAddress(pauser);
45 |
46 | super('Unpaused', data);
47 | }
48 | }
49 |
50 | @final
51 | export class OwnershipTransferStartedEvent extends NetEvent {
52 | constructor(currentOwner: Address, pendingOwner: Address) {
53 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
54 | data.writeAddress(currentOwner);
55 | data.writeAddress(pendingOwner);
56 |
57 | super('OwnershipTransferStarted', data);
58 | }
59 | }
60 |
61 | @final
62 | export class OwnershipTransferredEvent extends NetEvent {
63 | constructor(previousOwner: Address, newOwner: Address) {
64 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
65 | data.writeAddress(previousOwner);
66 | data.writeAddress(newOwner);
67 |
68 | super('OwnershipTransferred', data);
69 | }
70 | }
71 |
72 | @final
73 | export class MinterChangedEvent extends NetEvent {
74 | constructor(previousMinter: Address, newMinter: Address) {
75 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
76 | data.writeAddress(previousMinter);
77 | data.writeAddress(newMinter);
78 |
79 | super('MinterChanged', data);
80 | }
81 | }
82 |
83 | @final
84 | export class BlacklisterChangedEvent extends NetEvent {
85 | constructor(previousBlacklister: Address, newBlacklister: Address) {
86 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
87 | data.writeAddress(previousBlacklister);
88 | data.writeAddress(newBlacklister);
89 |
90 | super('BlacklisterChanged', data);
91 | }
92 | }
93 |
94 | @final
95 | export class PauserChangedEvent extends NetEvent {
96 | constructor(previousPauser: Address, newPauser: Address) {
97 | const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2);
98 | data.writeAddress(previousPauser);
99 | data.writeAddress(newPauser);
100 |
101 | super('PauserChanged', data);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/pegged-token/MyPeggedToken.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from '@btc-vision/as-bignum/assembly';
2 | import {
3 | Address,
4 | AddressMemoryMap,
5 | Blockchain,
6 | BytesWriter,
7 | Calldata,
8 | OP20InitParameters,
9 | OP20S,
10 | Revert,
11 | } from '@btc-vision/btc-runtime/runtime';
12 | import { CustodianChangedEvent } from '../shared-events/OracleEvents';
13 |
14 | const custodianPointer: u16 = Blockchain.nextPointer;
15 | const pendingCustodianPointer: u16 = Blockchain.nextPointer;
16 |
17 | @final
18 | export class MyPeggedToken extends OP20S {
19 | private readonly _custodianMap: AddressMemoryMap;
20 | private readonly _pendingCustodianMap: AddressMemoryMap;
21 |
22 | public constructor() {
23 | super();
24 | this._custodianMap = new AddressMemoryMap(custodianPointer);
25 | this._pendingCustodianMap = new AddressMemoryMap(pendingCustodianPointer);
26 | }
27 |
28 | public override onDeployment(calldata: Calldata): void {
29 | const custodian = calldata.readAddress();
30 |
31 | if (custodian.equals(Address.zero())) {
32 | throw new Revert('Invalid custodian');
33 | }
34 |
35 | const maxSupply: u256 = u256.fromU64(2100000000000000);
36 | const decimals: u8 = 8;
37 | const name: string = 'Wrapped BTC';
38 | const symbol: string = 'WBTC';
39 |
40 | this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
41 | this.initializePeg(custodian, u256.One, u64.MAX_VALUE);
42 |
43 | this._setCustodian(custodian);
44 |
45 | this.emitEvent(new CustodianChangedEvent(Address.zero(), custodian));
46 | }
47 |
48 | @method(
49 | { name: 'to', type: ABIDataTypes.ADDRESS },
50 | { name: 'amount', type: ABIDataTypes.UINT256 },
51 | )
52 | @emit('Minted')
53 | public mint(calldata: Calldata): BytesWriter {
54 | this._onlyCustodian();
55 |
56 | const to = calldata.readAddress();
57 | const amount = calldata.readU256();
58 |
59 | if (to.equals(Address.zero())) {
60 | throw new Revert('Invalid recipient');
61 | }
62 | if (amount.isZero()) {
63 | throw new Revert('Amount is zero');
64 | }
65 |
66 | this._mint(to, amount);
67 |
68 | return new BytesWriter(0);
69 | }
70 |
71 | @method(
72 | { name: 'from', type: ABIDataTypes.ADDRESS },
73 | { name: 'amount', type: ABIDataTypes.UINT256 },
74 | )
75 | @emit('Burned')
76 | public burnFrom(calldata: Calldata): BytesWriter {
77 | this._onlyCustodian();
78 |
79 | const from = calldata.readAddress();
80 | const amount = calldata.readU256();
81 |
82 | if (from.equals(Address.zero())) {
83 | throw new Revert('Invalid address');
84 | }
85 |
86 | const balance = this._balanceOf(from);
87 | if (balance < amount) {
88 | throw new Revert('Insufficient balance');
89 | }
90 |
91 | this._burn(from, amount);
92 |
93 | return new BytesWriter(0);
94 | }
95 |
96 | @method({ name: 'newCustodian', type: ABIDataTypes.ADDRESS })
97 | public transferCustodian(calldata: Calldata): BytesWriter {
98 | this._onlyCustodian();
99 |
100 | const newCustodian = calldata.readAddress();
101 | if (newCustodian.equals(Address.zero())) {
102 | throw new Revert('Invalid new custodian');
103 | }
104 |
105 | this._setPendingCustodian(newCustodian);
106 |
107 | return new BytesWriter(0);
108 | }
109 |
110 | @method()
111 | @emit('CustodianChanged')
112 | public acceptCustodian(_: Calldata): BytesWriter {
113 | const pending = this._getPendingCustodian();
114 | if (pending.equals(Address.zero())) {
115 | throw new Revert('No pending custodian');
116 | }
117 | if (!Blockchain.tx.sender.equals(pending)) {
118 | throw new Revert('Not pending custodian');
119 | }
120 |
121 | const previousCustodian = this._getCustodian();
122 | this._setCustodian(pending);
123 | this._setPendingCustodian(Address.zero());
124 |
125 | this.emitEvent(new CustodianChangedEvent(previousCustodian, pending));
126 |
127 | return new BytesWriter(0);
128 | }
129 |
130 | @method()
131 | @returns({ name: 'custodian', type: ABIDataTypes.ADDRESS })
132 | public custodian(_: Calldata): BytesWriter {
133 | const w = new BytesWriter(32);
134 | w.writeAddress(this._getCustodian());
135 | return w;
136 | }
137 |
138 | @method()
139 | @returns({ name: 'pendingCustodian', type: ABIDataTypes.ADDRESS })
140 | public pendingCustodian(_: Calldata): BytesWriter {
141 | const w = new BytesWriter(32);
142 | w.writeAddress(this._getPendingCustodian());
143 | return w;
144 | }
145 |
146 | private _getCustodian(): Address {
147 | const stored = this._custodianMap.get(Address.zero());
148 | if (stored.isZero()) return Address.zero();
149 | return this._u256ToAddress(stored);
150 | }
151 |
152 | private _setCustodian(addr: Address): void {
153 | this._custodianMap.set(Address.zero(), this._addressToU256(addr));
154 | }
155 |
156 | private _getPendingCustodian(): Address {
157 | const stored = this._pendingCustodianMap.get(Address.zero());
158 | if (stored.isZero()) return Address.zero();
159 | return this._u256ToAddress(stored);
160 | }
161 |
162 | private _setPendingCustodian(addr: Address): void {
163 | this._pendingCustodianMap.set(Address.zero(), this._addressToU256(addr));
164 | }
165 |
166 | private _onlyCustodian(): void {
167 | if (!Blockchain.tx.sender.equals(this._getCustodian())) {
168 | throw new Revert('Not custodian');
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ## GITATTRIBUTES FOR WEB PROJECTS
2 | #
3 | # These settings are for any web project.
4 | #
5 | # Details per file setting:
6 | # text These files should be normalized (i.e. convert CRLF to LF).
7 | # binary These files are binary and should be left untouched.
8 | #
9 | # Note that binary is a macro for -text -diff.
10 | ######################################################################
11 |
12 | # Auto detect
13 | ## Handle line endings automatically for files detected as
14 | ## text and leave all files detected as binary untouched.
15 | ## This will handle all files NOT defined below.
16 | * text=auto
17 |
18 | # Source code
19 | *.bash text eol=lf
20 | *.bat text eol=crlf
21 | *.cmd text eol=crlf
22 | *.coffee text
23 | *.css text diff=css
24 | *.htm text diff=html
25 | *.html text diff=html
26 | *.inc text
27 | *.ini text
28 | *.js text
29 | *.mjs text
30 | *.cjs text
31 | *.json text
32 | *.jsx text
33 | *.less text
34 | *.ls text
35 | *.map text -diff
36 | *.od text
37 | *.onlydata text
38 | *.php text diff=php
39 | *.pl text
40 | *.ps1 text eol=crlf
41 | *.py text diff=python
42 | *.rb text diff=ruby
43 | *.sass text
44 | *.scm text
45 | *.scss text diff=css
46 | *.sh text eol=lf
47 | .husky/* text eol=lf
48 | *.sql text
49 | *.styl text
50 | *.tag text
51 | *.ts text
52 | *.tsx text
53 | *.xml text
54 | *.xhtml text diff=html
55 |
56 | # Docker
57 | Dockerfile text
58 |
59 | # Documentation
60 | *.ipynb text eol=lf
61 | *.markdown text diff=markdown
62 | *.md text diff=markdown
63 | *.mdwn text diff=markdown
64 | *.mdown text diff=markdown
65 | *.mkd text diff=markdown
66 | *.mkdn text diff=markdown
67 | *.mdtxt text
68 | *.mdtext text
69 | *.txt text
70 | AUTHORS text
71 | CHANGELOG text
72 | CHANGES text
73 | CONTRIBUTING text
74 | COPYING text
75 | copyright text
76 | *COPYRIGHT* text
77 | INSTALL text
78 | license text
79 | LICENSE text
80 | NEWS text
81 | readme text
82 | *README* text
83 | TODO text
84 |
85 | # Templates
86 | *.dot text
87 | *.ejs text
88 | *.erb text
89 | *.haml text
90 | *.handlebars text
91 | *.hbs text
92 | *.hbt text
93 | *.jade text
94 | *.latte text
95 | *.mustache text
96 | *.njk text
97 | *.phtml text
98 | *.svelte text
99 | *.tmpl text
100 | *.tpl text
101 | *.twig text
102 | *.vue text
103 |
104 | # Configs
105 | *.cnf text
106 | *.conf text
107 | *.config text
108 | .editorconfig text
109 | .env text
110 | .gitattributes text
111 | .gitconfig text
112 | .htaccess text
113 | *.lock text -diff
114 | package.json text eol=lf
115 | package-lock.json text eol=lf -diff
116 | pnpm-lock.yaml text eol=lf -diff
117 | .prettierrc text
118 | yarn.lock text -diff
119 | *.toml text
120 | *.yaml text
121 | *.yml text
122 | browserslist text
123 | Makefile text
124 | makefile text
125 | # Fixes syntax highlighting on GitHub to allow comments
126 | tsconfig.json linguist-language=JSON-with-Comments
127 |
128 | # Heroku
129 | Procfile text
130 |
131 | # Graphics
132 | *.ai binary
133 | *.bmp binary
134 | *.eps binary
135 | *.gif binary
136 | *.gifv binary
137 | *.ico binary
138 | *.jng binary
139 | *.jp2 binary
140 | *.jpg binary
141 | *.jpeg binary
142 | *.jpx binary
143 | *.jxr binary
144 | *.pdf binary
145 | *.png binary
146 | *.psb binary
147 | *.psd binary
148 | # SVG treated as an asset (binary) by default.
149 | *.svg text
150 | # If you want to treat it as binary,
151 | # use the following line instead.
152 | # *.svg binary
153 | *.svgz binary
154 | *.tif binary
155 | *.tiff binary
156 | *.wbmp binary
157 | *.webp binary
158 |
159 | # Audio
160 | *.kar binary
161 | *.m4a binary
162 | *.mid binary
163 | *.midi binary
164 | *.mp3 binary
165 | *.ogg binary
166 | *.ra binary
167 |
168 | # Video
169 | *.3gpp binary
170 | *.3gp binary
171 | *.as binary
172 | *.asf binary
173 | *.asx binary
174 | *.avi binary
175 | *.fla binary
176 | *.flv binary
177 | *.m4v binary
178 | *.mng binary
179 | *.mov binary
180 | *.mp4 binary
181 | *.mpeg binary
182 | *.mpg binary
183 | *.ogv binary
184 | *.swc binary
185 | *.swf binary
186 | *.webm binary
187 |
188 | # Archives
189 | *.7z binary
190 | *.gz binary
191 | *.jar binary
192 | *.rar binary
193 | *.tar binary
194 | *.zip binary
195 |
196 | # Fonts
197 | *.ttf binary
198 | *.eot binary
199 | *.otf binary
200 | *.woff binary
201 | *.woff2 binary
202 |
203 | # Executables
204 | *.exe binary
205 | *.pyc binary
206 | # Prevents massive diffs caused by vendored, minified files
207 | **/.yarn/releases/** binary
208 | **/.yarn/plugins/** binary
209 |
210 | # RC files (like .babelrc or .eslintrc)
211 | *.*rc text
212 |
213 | # Ignore files (like .npmignore or .gitignore)
214 | *.*ignore text
215 |
216 | # Prevents massive diffs from built files
217 | dist/* binary
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deploying and Customizing an OP_20 Token on OP_NET
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 |
10 | [](https://github.com/prettier/prettier)
11 |
12 | ## Prerequisites
13 |
14 | - Ensure you have [Node.js](https://nodejs.org/en/download/prebuilt-installer) and [npm](https://www.npmjs.com/)
15 | installed on your computer.
16 |
17 | ## Step-by-Step Guide
18 |
19 | ### 1. Install OP_WALLET Chrome Extension
20 |
21 | - Download and install the [OP_WALLET Chrome Extension](https://opnet.org).
22 | - Set up the wallet and switch the network to Regtest.
23 |
24 | ### 2. Obtain Regtest Bitcoin
25 |
26 | - If you don't have any Regtest Bitcoin, get some from [this faucet](https://faucet.opnet.org/).
27 |
28 | ### 3. Download OP_20 Template Contract
29 |
30 | - Clone the [OP_20 template contract](https://github.com/btc-vision/OP_20) repository:
31 | ```sh
32 | git clone https://github.com/btc-vision/OP_20.git
33 | ```
34 |
35 | ### 4. Edit Token Details
36 |
37 | This step is crucial for customizing your OP_20 token. You will need to adjust several key properties such as
38 | `maxSupply`, `decimals`, `name`, and `symbol`.
39 |
40 | #### **Understanding Token Properties**
41 |
42 | Here’s what each property means and how you can customize it:
43 |
44 | 1. **`maxSupply`**:
45 |
46 | - This defines the total supply of your token.
47 | - It’s a `u256` value representing the maximum number of tokens that will ever exist.
48 | - The number should include the full number of decimals.
49 | - **Example**: If you want a total supply of 1,000,000 tokens with 18 decimals, the value should be
50 | `1000000000000000000000000`.
51 |
52 | ```typescript
53 | const maxSupply: u256 = u256.fromString('1000000000000000000000000000'); // Your max supply. (Here, 1 billion tokens)
54 | ```
55 |
56 | 2. **`decimals`**:
57 |
58 | - This property defines how divisible your token is.
59 | - A value of `18` means the token can be divided down to 18 decimal places, similar to how Ethereum handles its tokens.
60 |
61 | ```typescript
62 | const decimals: u8 = 18; // Your decimals
63 | ```
64 |
65 | 3. **`name`**:
66 |
67 | - The `name` is a string representing the full name of your token.
68 | - This will be displayed in wallets and exchanges.
69 |
70 | ```typescript
71 | const name: string = 'Test'; // Your token name
72 | ```
73 |
74 | 4. **`symbol`**:
75 |
76 | - The `symbol` is a short string representing the ticker symbol of your token.
77 | - Similar to how "BTC" represents Bitcoin.
78 |
79 | ```typescript
80 | const symbol: string = 'TEST'; // Your token symbol
81 | ```
82 |
83 | #### **Modifying the Contract Code**
84 |
85 | Open the `OP_20` template repository in your IDE or text editor and navigate to `src/contracts/token/MyToken.ts`. Look
86 | for the following section in the `onInstantiated` method:
87 |
88 | ```typescript
89 | const maxSupply: u256 = u256.fromString('1000000000000000000000000000'); // Your max supply. (Here, 1 billion tokens)
90 | const decimals: u8 = 18; // Your decimals.
91 | const name: string = 'Test'; // Your token name.
92 | const symbol: string = 'TEST'; // Your token symbol.
93 | ```
94 |
95 | Modify the values as needed for your token.
96 |
97 | ### 5. Install Dependencies and Build
98 |
99 | After customizing your token's properties, build the contract:
100 |
101 | - Open your terminal and navigate to the location of the downloaded `OP_20` template folder.
102 | - Run the following commands:
103 |
104 | ```sh
105 | npm install
106 | npm run build:token
107 | ```
108 |
109 | - After building, a `build` folder will be created in the root of the `OP_20` folder. Look for `[nameoftoken].wasm` for
110 | the compiled contract.
111 |
112 | ### 6. Deploy the Token Contract
113 |
114 | - Open the OP_WALLET extension and select the "deploy" option.
115 | - Drag your `.wasm` file or click to choose it.
116 | - Send your transaction to deploy the token contract onto Bitcoin with OP_NET.
117 |
118 | ### 7. Add Liquidity on Motoswap
119 |
120 | - Copy the token address from your OP_WALLET.
121 | - Go to [Motoswap](https://motoswap.org/pool) and paste your token address into the top or bottom box.
122 | - Enter the amount of tokens you wish to add to the liquidity pool.
123 | - Select the other side of the liquidity pair (e.g., WBTC) and enter the amount of tokens you wish to add.
124 | - Click "Add Liquidity".
125 |
126 | Your token is now tradeable on Motoswap!
127 |
128 | ---
129 |
130 | ## Customizing Your Token Further
131 |
132 | Now that you've set up the basic token properties, you can add additional functionality to your OP_20 token contract.
133 | Here are some common customizations:
134 |
135 | ### Adding Custom Methods
136 |
137 | To add custom functionality to your token, you can define new methods in your contract. For example, let's say you want
138 | to add an "airdrop" function that distributes tokens to multiple addresses.
139 |
140 | #### Example: Airdrop Function
141 |
142 | ```typescript
143 | public override callMethod(method: Selector, calldata: Calldata): BytesWriter {
144 | switch (method) {
145 | case encodeSelector('airdrop()'):
146 | return this.airdrop(calldata);
147 | default:
148 | return super.callMethod(method, calldata);
149 | }
150 | }
151 |
152 | private airdrop(calldata: Calldata): BytesWriter {
153 | const drops: Map = calldata.readAddressValueTuple();
154 |
155 | const addresses: Address[] = drops.keys();
156 | for (let i: i32 = 0; i < addresses.length; i++) {
157 | const address = addresses[i];
158 | const amount = drops.get(address);
159 |
160 | this._mint(address, amount);
161 | }
162 |
163 | const writer: BytesWriter = new BytesWriter(BOOLEAN_BYTE_LENGTH);
164 | writer.writeBoolean(true);
165 |
166 | return writer;
167 | }
168 | ```
169 |
170 | ### Overriding Methods
171 |
172 | You may want to override some of the existing methods in the `DeployableOP_20` base class. For example, you might want
173 | to add additional logic when minting tokens.
174 |
175 | #### Example: Overriding `_mint` Method
176 |
177 | ```typescript
178 | protected _mint(to: Address, amount: u256): void {
179 | super._mint(to, amount);
180 |
181 | // Add custom logic here
182 | Blockchain.log(`Minted ${amount.toString()} tokens to ${to.toString()}`); // Only work inside OP_NET Uint Test Framework
183 | }
184 | ```
185 |
186 | ### Creating Events
187 |
188 | Events in OP_NET allow you to emit signals that external observers can listen to. These are useful for tracking specific
189 | actions within your contract, such as token transfers or approvals.
190 |
191 | #### Example: Transfer Event
192 |
193 | ```typescript
194 | class TransferEvent extends NetEvent {
195 | constructor(from: Address, to: Address, amount: u256) {
196 | const writer = new BytesWriter(ADDRESS_BYTE_LENGTH * 2 + U256_BYTE_LENGTH);
197 | writer.writeAddress(from);
198 | writer.writeAddress(to);
199 | writer.writeU256(amount);
200 | super('Transfer', writer);
201 | }
202 | }
203 |
204 | class MyToken extends DeployableOP_20 {
205 | public transfer(to: Address, amount: u256): void {
206 | const from: Address = Blockchain.sender;
207 | this._mint(to, amount);
208 | this.emitEvent(new TransferEvent(from, to, amount));
209 | }
210 | }
211 | ```
212 |
213 | ### Implementing Additional Security Measures
214 |
215 | If you want to add more control over who can call certain methods or add advanced features like pausing token transfers,
216 | you can implement access control mechanisms.
217 |
218 | #### Example: Only Owner Modifier
219 |
220 | ```typescript
221 | public mint(to: Address, amount: u256): void {
222 | this.onlyOwner(Blockchain.sender); // Restrict minting to the contract owner
223 | this._mint(to, amount);
224 | }
225 | ```
226 |
227 | ---
228 |
229 | ## Differences Between Solidity and AssemblyScript on OP_NET
230 |
231 | ### Constructor Behavior
232 |
233 | - **Solidity:** The constructor runs only once at the time of contract deployment and is used for initializing contract
234 | state.
235 | - **AssemblyScript on OP_NET:** The constructor runs every time the contract is instantiated. Use `onInstantiated()` for
236 | initialization that should occur only once.
237 |
238 | ### State Management
239 |
240 | - **Solidity:** Variables declared at the contract level are automatically persistent and are stored in the contract's
241 | state.
242 | - **AssemblyScript on OP_NET:** Persistent state must be managed explicitly using storage classes like `StoredU256`,
243 | `StoredBoolean`, and `StoredString`.
244 |
245 | ### Method Overriding
246 |
247 | - **Solidity:** Method selectors are built-in, and overriding them is straightforward.
248 | - **AssemblyScript on OP_NET:** Method selectors are manually defined using functions like `encodeSelector()`, and
249 | method overriding is handled in `callMethod`.
250 |
251 | ### Event Handling
252 |
253 | - **Solidity:** Events are declared and emitted using the `emit` keyword.
254 | - **AssemblyScript on OP_NET:** Events are custom classes derived from `NetEvent` and are emitted using the `emitEvent`
255 | function.
256 |
257 | ---
258 |
259 | ## Advanced Features
260 |
261 | ### Implementing Additional Custom Logic
262 |
263 | The OPNet runtime allows you to implement complex logic in your token contract. For example, you can add functionality
264 | such as token freezing, custom transaction fees, or governance mechanisms.
265 |
266 | These features are implemented by extending the base `DeployableOP_20` or `OP_20` class and overriding its methods as
267 | needed.
268 |
269 | ---
270 |
271 | ## Additional Documentation
272 |
273 | For more detailed explanations on specific topics related to the OPNet runtime, refer to the following documentation:
274 |
275 | - [OPNet Runtime Documentation](https://github.com/btc-vision/btc-runtime/tree/main)
276 | - [Blockchain.md](https://github.com/btc-vision/btc-runtime/blob/main/docs/Blockchain.md)
277 | - [Contract.md](https://github.com/btc-vision/btc-runtime/blob/main/docs/Contract.md)
278 | - [Events.md](https://github.com/btc-vision/btc-runtime/blob/main/docs/Events.md)
279 | - [Pointers.md](https://github.com/btc-vision/btc-runtime/blob/main/docs/Pointers.md)
280 | - [Storage.md](https://github.com/btc-vision/btc-runtime/blob/main/docs/Storage.md)
281 |
282 | ---
283 |
284 | ## License
285 |
286 | This project is licensed under the MIT License. View the full license [here](LICENSE.md).
287 |
--------------------------------------------------------------------------------
/src/multi-oracle-stablecoin/MyMultiOracleStable.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from '@btc-vision/as-bignum/assembly';
2 | import {
3 | Address,
4 | AddressMemoryMap,
5 | Blockchain,
6 | BytesWriter,
7 | Calldata,
8 | EMPTY_POINTER,
9 | OP20InitParameters,
10 | OP20S,
11 | Revert,
12 | SafeMath,
13 | StoredU256,
14 | } from '@btc-vision/btc-runtime/runtime';
15 | import {
16 | OracleAddedEvent,
17 | OracleRemovedEvent,
18 | PriceAggregatedEvent,
19 | PriceSubmittedEvent,
20 | } from '../shared-events/OracleEvents';
21 |
22 | const oracleCountPointer: u16 = Blockchain.nextPointer;
23 | const minOraclesPointer: u16 = Blockchain.nextPointer;
24 | const maxDeviationPointer: u16 = Blockchain.nextPointer;
25 | const submissionWindowPointer: u16 = Blockchain.nextPointer;
26 | const oracleSubmissionsPointer: u16 = Blockchain.nextPointer;
27 | const oracleTimestampsPointer: u16 = Blockchain.nextPointer;
28 | const oracleActivePointer: u16 = Blockchain.nextPointer;
29 | const adminPointer: u16 = Blockchain.nextPointer;
30 |
31 | @final
32 | export class MultiOracleStablecoin extends OP20S {
33 | private readonly _oracleCount: StoredU256;
34 | private readonly _minOracles: StoredU256;
35 | private readonly _maxDeviation: StoredU256;
36 | private readonly _submissionWindow: StoredU256;
37 | private readonly _oracleSubmissions: AddressMemoryMap;
38 | private readonly _oracleTimestamps: AddressMemoryMap;
39 | private readonly _oracleActive: AddressMemoryMap;
40 | private readonly _adminMap: AddressMemoryMap;
41 |
42 | public constructor() {
43 | super();
44 | this._oracleCount = new StoredU256(oracleCountPointer, EMPTY_POINTER);
45 | this._minOracles = new StoredU256(minOraclesPointer, EMPTY_POINTER);
46 | this._maxDeviation = new StoredU256(maxDeviationPointer, EMPTY_POINTER);
47 | this._submissionWindow = new StoredU256(submissionWindowPointer, EMPTY_POINTER);
48 | this._oracleSubmissions = new AddressMemoryMap(oracleSubmissionsPointer);
49 | this._oracleTimestamps = new AddressMemoryMap(oracleTimestampsPointer);
50 | this._oracleActive = new AddressMemoryMap(oracleActivePointer);
51 | this._adminMap = new AddressMemoryMap(adminPointer);
52 | }
53 |
54 | public override onDeployment(calldata: Calldata): void {
55 | const admin = calldata.readAddress();
56 | const initialRate = calldata.readU256();
57 | const minOracles = calldata.readU64();
58 | const maxDeviation = calldata.readU64();
59 | const submissionWindow = calldata.readU64();
60 |
61 | if (admin.equals(Address.zero())) {
62 | throw new Revert('Invalid admin');
63 | }
64 |
65 | if (initialRate.isZero()) {
66 | throw new Revert('Invalid initial rate');
67 | }
68 |
69 | if (minOracles == 0) {
70 | throw new Revert('Invalid min oracles');
71 | }
72 |
73 | if (maxDeviation == 0 || maxDeviation > 1000) {
74 | throw new Revert('Invalid max deviation');
75 | }
76 |
77 | if (submissionWindow == 0) {
78 | throw new Revert('Invalid submission window');
79 | }
80 |
81 | const maxSupply: u256 = u256.Max;
82 | const decimals: u8 = 8;
83 | const name: string = 'USD Stablecoin';
84 | const symbol: string = 'opUSD';
85 |
86 | this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
87 | this.initializePeg(admin, initialRate, submissionWindow * 2);
88 |
89 | this._setAdmin(admin);
90 | this._minOracles.value = u256.fromU64(minOracles);
91 | this._maxDeviation.value = u256.fromU64(maxDeviation);
92 | this._submissionWindow.value = u256.fromU64(submissionWindow);
93 | }
94 |
95 | @method({ name: 'oracle', type: ABIDataTypes.ADDRESS })
96 | @emit('OracleAdded')
97 | public addOracle(calldata: Calldata): BytesWriter {
98 | this._onlyAdmin();
99 |
100 | const oracle = calldata.readAddress();
101 | if (oracle.equals(Address.zero())) {
102 | throw new Revert('Invalid oracle');
103 | }
104 |
105 | const alreadyActive = this._oracleActive.get(oracle);
106 | if (!alreadyActive.isZero()) {
107 | throw new Revert('Oracle exists');
108 | }
109 |
110 | this._oracleActive.set(oracle, u256.One);
111 | this._oracleCount.value = SafeMath.add(this._oracleCount.value, u256.One);
112 |
113 | this.emitEvent(new OracleAddedEvent(oracle, Blockchain.tx.sender));
114 |
115 | return new BytesWriter(0);
116 | }
117 |
118 | @method({ name: 'oracle', type: ABIDataTypes.ADDRESS })
119 | @emit('OracleRemoved')
120 | public removeOracle(calldata: Calldata): BytesWriter {
121 | this._onlyAdmin();
122 |
123 | const oracle = calldata.readAddress();
124 | const active = this._oracleActive.get(oracle);
125 | if (active.isZero()) {
126 | throw new Revert('Oracle not active');
127 | }
128 |
129 | this._oracleActive.set(oracle, u256.Zero);
130 | this._oracleCount.value = SafeMath.sub(this._oracleCount.value, u256.One);
131 |
132 | this.emitEvent(new OracleRemovedEvent(oracle, Blockchain.tx.sender));
133 |
134 | return new BytesWriter(0);
135 | }
136 |
137 | @method({ name: 'price', type: ABIDataTypes.UINT256 })
138 | @emit('PriceSubmitted')
139 | public submitPrice(calldata: Calldata): BytesWriter {
140 | const sender = Blockchain.tx.sender;
141 | const active = this._oracleActive.get(sender);
142 | if (active.isZero()) {
143 | throw new Revert('Not an oracle');
144 | }
145 |
146 | const price = calldata.readU256();
147 | if (price.isZero()) {
148 | throw new Revert('Invalid price');
149 | }
150 |
151 | const blockNumber = Blockchain.block.number;
152 |
153 | this._oracleSubmissions.set(sender, price);
154 | this._oracleTimestamps.set(sender, u256.fromU64(blockNumber));
155 |
156 | this.emitEvent(new PriceSubmittedEvent(sender, price, blockNumber));
157 |
158 | return new BytesWriter(0);
159 | }
160 |
161 | @method({ name: 'oracles', type: ABIDataTypes.ARRAY_OF_ADDRESSES })
162 | @emit('PriceAggregated')
163 | public aggregatePrice(calldata: Calldata): BytesWriter {
164 | const oracleCount = calldata.readU32();
165 | if (u256.fromU32(oracleCount) < this._minOracles.value) {
166 | throw new Revert('Not enough oracles');
167 | }
168 |
169 | const currentBlock = Blockchain.block.number;
170 | const window = this._submissionWindow.value.toU64();
171 | const maxDev = this._maxDeviation.value;
172 |
173 | const prices = new Array();
174 |
175 | for (let i: u32 = 0; i < oracleCount; i++) {
176 | const oracle = calldata.readAddress();
177 |
178 | const active = this._oracleActive.get(oracle);
179 | if (active.isZero()) continue;
180 |
181 | const timestamp = this._oracleTimestamps.get(oracle).toU64();
182 | if (currentBlock > timestamp + window) continue;
183 |
184 | const price = this._oracleSubmissions.get(oracle);
185 | if (price.isZero()) continue;
186 |
187 | prices.push(price);
188 | }
189 |
190 | const validCount: u32 = prices.length;
191 |
192 | if (u256.fromU32(validCount) < this._minOracles.value) {
193 | throw new Revert('Insufficient valid submissions');
194 | }
195 |
196 | for (let i = 0; i < prices.length - 1; i++) {
197 | for (let j = 0; j < prices.length - i - 1; j++) {
198 | if (prices[j] > prices[j + 1]) {
199 | const temp = prices[j];
200 | prices[j] = prices[j + 1];
201 | prices[j + 1] = temp;
202 | }
203 | }
204 | }
205 |
206 | const median = prices[prices.length / 2];
207 |
208 | const basisPoints = u256.fromU64(10000);
209 | for (let i = 0; i < prices.length; i++) {
210 | const price = prices[i];
211 | let deviation: u256;
212 | if (price > median) {
213 | deviation = SafeMath.div(
214 | SafeMath.mul(SafeMath.sub(price, median), basisPoints),
215 | median,
216 | );
217 | } else {
218 | deviation = SafeMath.div(
219 | SafeMath.mul(SafeMath.sub(median, price), basisPoints),
220 | median,
221 | );
222 | }
223 |
224 | if (deviation > maxDev) {
225 | throw new Revert('Deviation too high');
226 | }
227 | }
228 |
229 | this._pegRate.value = median;
230 | this._pegUpdatedAt.value = u256.fromU64(currentBlock);
231 |
232 | this.emitEvent(new PriceAggregatedEvent(median, validCount, currentBlock));
233 |
234 | return new BytesWriter(0);
235 | }
236 |
237 | @method(
238 | { name: 'to', type: ABIDataTypes.ADDRESS },
239 | { name: 'amount', type: ABIDataTypes.UINT256 },
240 | )
241 | @emit('Minted')
242 | public mint(calldata: Calldata): BytesWriter {
243 | this._onlyAdmin();
244 |
245 | const to = calldata.readAddress();
246 | const amount = calldata.readU256();
247 |
248 | if (to.equals(Address.zero())) {
249 | throw new Revert('Invalid recipient');
250 | }
251 | if (amount.isZero()) {
252 | throw new Revert('Amount is zero');
253 | }
254 |
255 | this._mint(to, amount);
256 |
257 | return new BytesWriter(0);
258 | }
259 |
260 | @method()
261 | @returns({ name: 'count', type: ABIDataTypes.UINT256 })
262 | public oracleCount(_: Calldata): BytesWriter {
263 | const w = new BytesWriter(32);
264 | w.writeU256(this._oracleCount.value);
265 | return w;
266 | }
267 |
268 | @method()
269 | @returns({ name: 'min', type: ABIDataTypes.UINT256 })
270 | public minOracles(_: Calldata): BytesWriter {
271 | const w = new BytesWriter(32);
272 | w.writeU256(this._minOracles.value);
273 | return w;
274 | }
275 |
276 | @method({ name: 'oracle', type: ABIDataTypes.ADDRESS })
277 | @returns({ name: 'active', type: ABIDataTypes.BOOL })
278 | public isOracleActive(calldata: Calldata): BytesWriter {
279 | const oracle = calldata.readAddress();
280 | const w = new BytesWriter(1);
281 | w.writeBoolean(!this._oracleActive.get(oracle).isZero());
282 | return w;
283 | }
284 |
285 | @method({ name: 'oracle', type: ABIDataTypes.ADDRESS })
286 | @returns({ name: 'price', type: ABIDataTypes.UINT256 })
287 | public oracleSubmission(calldata: Calldata): BytesWriter {
288 | const oracle = calldata.readAddress();
289 | const w = new BytesWriter(32);
290 | w.writeU256(this._oracleSubmissions.get(oracle));
291 | return w;
292 | }
293 |
294 | @method()
295 | @returns({ name: 'admin', type: ABIDataTypes.ADDRESS })
296 | public admin(_: Calldata): BytesWriter {
297 | const w = new BytesWriter(32);
298 | w.writeAddress(this._getAdmin());
299 | return w;
300 | }
301 |
302 | private _getAdmin(): Address {
303 | const stored = this._adminMap.get(Address.zero());
304 | if (stored.isZero()) return Address.zero();
305 | return this._u256ToAddress(stored);
306 | }
307 |
308 | private _setAdmin(addr: Address): void {
309 | this._adminMap.set(Address.zero(), this._addressToU256(addr));
310 | }
311 |
312 | private _onlyAdmin(): void {
313 | if (!Blockchain.tx.sender.equals(this._getAdmin())) {
314 | throw new Revert('Not admin');
315 | }
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/docs/OP_20.md:
--------------------------------------------------------------------------------
1 | # The OP20 Standard
2 |
3 | ## Table of Contents
4 |
5 | 1. [Introduction: Why Token Standards Matter](#introduction-why-token-standards-matter)
6 | 2. [Understanding the Core Improvements](#understanding-the-core-improvements)
7 | - 2.1 [The Safe Transfer](#the-safe-transfer)
8 | - 2.2 [The Approval System](#the-approval-system)
9 | - 2.3 [Maximum Supply](#maximum-supply)
10 | - 2.4 [Metadata](#metadata)
11 | 3. [Technical Deep Dive](#technical-deep-dive)
12 | - 3.1 [Domain Separation and Structured Signatures](#domain-separation-and-structured-signatures)
13 | - 3.2 [Nonce Management: Preventing Replay Attacks](#nonce-management-preventing-replay-attacks)
14 | - 3.3 [The Receiver Hook Pattern in Detail](#the-receiver-hook-pattern-in-detail)
15 | - 3.4 [Advanced Allowance Management](#advanced-allowance-management)
16 | 4. [Real-World Scenarios](#real-world-scenarios)
17 | - 4.1 [Scenario 1: Decentralized Exchange Trading](#scenario-1-decentralized-exchange-trading)
18 | - 4.2 [Scenario 2: Multi-Signature Wallets and Team Treasuries](#scenario-2-multi-signature-wallets-and-team-treasuries)
19 | - 4.3 [Scenario 3: Subscription Services and Recurring Payments](#scenario-3-subscription-services-and-recurring-payments)
20 | 5. [Security Considerations](#security-considerations)
21 | - 5.1 [The Schnorr Signature Advantage](#the-schnorr-signature-advantage)
22 | - 5.2 [Comprehensive Input Validation](#comprehensive-input-validation)
23 | 6. [Migration Considerations: Moving from ERC20 to OP20](#migration-considerations-moving-from-erc20-to-op20)
24 | 7. [Conclusion](#conclusion)
25 |
26 | ---
27 |
28 | ## Introduction: Why Token Standards Matter
29 |
30 | Imagine you're creating a new currency for a global marketplace. You need everyone - from individual users to massive
31 | exchanges - to understand exactly how your currency works. Without clear rules, chaos would ensue. Some vendors might
32 | lose money in failed transactions, while others might accidentally destroy currency by sending it to the wrong place.
33 | This is precisely why token standards exist in blockchain technology.
34 |
35 | The ERC20 standard, introduced on Ethereum in 2015, became the first widely adopted set of rules for creating digital
36 | tokens. Think of it as the "version 1.0" of blockchain tokens - functional but with some rough edges discovered through
37 | years of real-world use. The OP20 standard represents "version 2.0," incorporating lessons learned from billions of
38 | dollars worth of ERC20 transactions and addressing the pain points that users and developers have encountered.
39 |
40 | ## Understanding the Core Improvements
41 |
42 | ### The Safe Transfer
43 |
44 | In the ERC20 world, sending tokens works like dropping a letter in a mailbox. You put the address on the envelope and
45 | hope it arrives safely. If you accidentally write the wrong address or send it to an abandoned building, your letter (
46 | and tokens) disappears forever. Billions of dollars worth of tokens have been lost this way when users accidentally sent
47 | them to smart contracts that couldn't process them.
48 |
49 | The OP20 standard transforms this process into something more like certified mail with delivery confirmation. When you
50 | send tokens to a smart contract address, the protocol doesn't just drop them off and leave. Instead, it knocks on the
51 | door and says, "I have a delivery of 100 tokens for you. Can you handle these?" The receiving contract must explicitly
52 | respond, "Yes, I can receive these tokens, and I know what to do with them." If the contract can't respond properly -
53 | perhaps because it's an old contract that doesn't understand tokens - the transfer is cancelled and your tokens return
54 | safely to your wallet.
55 |
56 | This safety mechanism extends even further. When contracts receive OP20 tokens, they also receive additional
57 | information: who originally owned these tokens, who initiated the transfer, and any special instructions. It's like
58 | receiving a package with a detailed shipping manifest and handling instructions, rather than just having a box dumped on
59 | your doorstep.
60 |
61 | ### The Approval System
62 |
63 | The way ERC20 handles spending permissions resembles writing blank checks. If you want to let someone spend 100 of your
64 | tokens, you write them a check for 100. But if you later want to change that to 150, you have to void the first check
65 | and write a new one for 150. During that brief moment between voiding the old check and writing the new one, a crafty
66 | person watching could cash both checks, stealing 250 tokens instead of the intended 150.
67 |
68 | OP20 system think in terms of adjustments rather than absolutes. Instead of writing new
69 | checks, you simply say "increase their spending limit by 50" or "decrease it by 30." There's never a moment where
70 | multiple checks could be valid simultaneously. It's like having a credit card where you can smoothly adjust someone's
71 | authorized user limit up or down without any security gaps.
72 |
73 | This becomes even more powerful with signature-based approvals. Imagine being able to write a permission slip that
74 | says, "The holder of this note can increase their spending allowance by 1000 tokens, but only if they use it before next
75 | Tuesday." You can create this permission without paying any transaction fees, and the recipient can use it when they
76 | need it. This enables sophisticated interactions where you could, for example, authorize a decentralized exchange to
77 | take tokens for a trade without making a separate transaction first.
78 |
79 | ### Maximum Supply
80 |
81 | Many ERC20 tokens operate on an honor system regarding their maximum supply. The creators promise they won't create more
82 | than a certain amount, but technically, the code might allow them to mint unlimited tokens. It's like a government that
83 | promises not to print too much money but keeps the printing presses running in the basement, just in case.
84 |
85 | OP20 takes a different approach. When a token is created, its maximum supply is locked in a cryptographic vault that no
86 | one - not even the creators - can open. Every time new tokens are created (minted), the protocol checks this immutable
87 | limit. If creating new tokens would exceed the maximum supply, the operation fails automatically. This provides users
88 | with mathematical certainty about the token's economics, similar to how Bitcoin's 21 million coin limit is encoded into
89 | its protocol.
90 |
91 | ### Metadata
92 |
93 | Getting information about an ERC20 token resembles playing twenty questions. You ask for the name, then the symbol, then
94 | the total supply, then the decimals - each question requiring a separate interaction with the blockchain. It's
95 | inefficient and time-consuming, like having to call different departments of a company to get basic information about a
96 | product.
97 |
98 | OP20 consolidates all essential token information into a single, comprehensive response. One query returns everything:
99 | the token's name, symbol, decimal places, current supply, maximum supply, icon, and cryptographic verification
100 | information. It's the difference between visiting seven different offices to gather documents versus receiving a
101 | complete information packet at a single window.
102 |
103 | ## Technical Deep Dive
104 |
105 | ### Domain Separation and Structured Signatures
106 |
107 | When you sign a message in OP20, you're not just scribbling your signature on any piece of paper. The protocol
108 | constructs a highly structured document that includes multiple layers of context. Think of it as the difference between
109 | signing a blank piece of paper (dangerous) versus signing a formal contract with your name, date, specific terms, and
110 | legal jurisdiction clearly stated (safe).
111 |
112 | The domain separator acts like a unique serial number for each token deployment. It combines the token's name, the
113 | blockchain it's on, the specific protocol version, and the contract's address into a unique identifier. This prevents a
114 | signature meant for "Banana Token on Network A" from being used on "Banana Token on Network B," even if they have the
115 | same name. It's similar to how a check written for a Bank of America account can't be cashed at Wells Fargo, even if the
116 | account numbers happen to match.
117 |
118 | ### Nonce Management: Preventing Replay Attacks
119 |
120 | Every address in OP20 has a nonce - think of it as a personal transaction counter. Each time you create a signature for
121 | an off-chain operation, it includes your current nonce value. Once that signature is used, your nonce increases by one,
122 | making that signature permanently invalid.
123 |
124 | This system prevents replay attacks, where someone might try to reuse an old authorization. Imagine if every time you
125 | wrote a check, it included a sequence number, and your bank would only accept checks in exact sequential order. Even if
126 | someone found an old check you wrote, they couldn't cash it because its sequence number would be too low. That's exactly
127 | how OP20's nonce system protects against signature replay.
128 |
129 | ### The Receiver Hook Pattern in Detail
130 |
131 | The receiver hook pattern works like a sophisticated delivery protocol. When tokens arrive at a smart contract, the
132 | following conversation occurs:
133 |
134 | 1. The OP20 token contract says: "Hello, Contract X! User A is sending you 100 tokens, initiated by User B. Here's some
135 | additional data they wanted to include."
136 |
137 | 2. Contract X must respond with exactly the right phrase: "Yes, I received 100 OP20 tokens and I know how to handle
138 | them."
139 |
140 | 3. If Contract X responds with anything else - or doesn't respond at all - the OP20 contract cancels the transfer.
141 |
142 | This pattern enables powerful composability. A decentralized exchange could automatically execute a trade when it
143 | receives tokens, a lending protocol could immediately deposit tokens into a yield-generating vault, or a payment
144 | splitter could distribute tokens among multiple recipients - all triggered by the initial transfer.
145 |
146 | ### Advanced Allowance Management
147 |
148 | The OP20 allowance system supports several sophisticated patterns that go beyond simple spending permissions. The
149 | protocol recognizes "infinite" allowances - when you trust a contract completely (like your own wallet software), you
150 | can grant it unlimited spending power that never decreases. This optimization reduces transaction costs for trusted
151 | integrations.
152 |
153 | The signature-based allowance system includes expiration times and careful structuring to prevent abuse. When you sign
154 | an allowance increase, you're signing a message that says: "I, owner of address 0x123, authorize address 0x456 to
155 | increase their spending allowance by 1000 tokens. This authorization uses nonce 42 and expires at timestamp 1234567890."
156 | This precision prevents any ambiguity or potential for manipulation.
157 |
158 | ## Real-World Scenarios
159 |
160 | ### Scenario 1: Decentralized Exchange Trading
161 |
162 | With ERC20 tokens, trading on a decentralized exchange follows this cumbersome process:
163 |
164 | 1. Send a transaction to approve the exchange to spend your tokens (costs gas, takes time)
165 | 2. Wait for the approval to be confirmed on the blockchain
166 | 3. Send another transaction to execute your trade (costs gas again, takes more time)
167 |
168 | With OP20 tokens, the process becomes seamless:
169 |
170 | 1. Sign a message authorizing the trade (free, instant)
171 | 2. Submit your order to the exchange
172 | 3. The exchange bundles your authorization and trade execution into a single transaction
173 |
174 | This not only saves money and time but also eliminates the risk of front-running, where bots might see your approval and
175 | trade against you before your transaction completes.
176 |
177 | ### Scenario 2: Multi-Signature Wallets and Team Treasuries
178 |
179 | Consider a company treasury that requires multiple signatures for spending. With ERC20, each signer must submit a
180 | separate blockchain transaction, paying gas fees each time. With OP20's signature aggregation capabilities, all signers
181 | can create their signatures offline, combine them, and submit a single transaction. It's like collecting all required
182 | signatures on a document before going to the bank, rather than having each person visit the bank separately.
183 |
184 | ### Scenario 3: Subscription Services and Recurring Payments
185 |
186 | OP20's signature-based allowances enable innovative payment patterns. A subscription service could request a signature
187 | that allows them to withdraw 10 tokens per month, with each withdrawal requiring a new nonce. Users maintain control -
188 | they can cancel anytime by refusing to sign new allowances - while services get the predictability they need. The
189 | expiration feature ensures that forgotten subscriptions don't continue indefinitely.
190 |
191 | ## Security Considerations
192 |
193 | ### The Schnorr Signature Advantage
194 |
195 | OP20's use of Schnorr signatures instead of ECDSA (used in Ethereum) provides several security benefits. Schnorr
196 | signatures are like tamper-evident seals - any attempt to modify them breaks them completely, making forgery impossible.
197 | They're also more efficient, producing smaller signatures that cost less to verify while providing stronger security
198 | guarantees.
199 |
200 | The mathematical properties of Schnorr signatures enable future innovations like signature aggregation, where multiple
201 | signatures can be combined into one. This could eventually allow complex multi-party transactions to be as efficient as
202 | simple transfers, opening new possibilities for decentralized governance and group decision-making.
203 |
204 | ### Comprehensive Input Validation
205 |
206 | OP20 implements defense-in-depth security principles. Every operation validates inputs thoroughly before making any
207 | changes. The protocol checks that addresses are valid and not special reserved addresses (like the zero address),
208 | amounts don't exceed balances or cause mathematical overflows, signatures match their expected format and haven't
209 | expired, and operations maintain the token's economic invariants (like maximum supply).
210 |
211 | This comprehensive validation prevents entire categories of bugs that have plagued ERC20 tokens. Users can't
212 | accidentally burn tokens by sending them to invalid addresses, mathematical errors can't create tokens out of thin air,
213 | and replay attacks can't drain wallets through reused signatures.
214 |
215 | ## Migration Considerations: Moving from ERC20 to OP20
216 |
217 | For projects considering migrating from ERC20 to OP20, the transition requires careful planning but offers significant
218 | benefits. The familiar concepts remain - tokens can still be transferred, allowances still control spending, and basic
219 | economic properties persist. However, the enhanced safety features and new capabilities require updates to supporting
220 | infrastructure.
221 |
222 | Wallets need to implement the new signature schemes and understand the incremental allowance model. Exchanges must
223 | implement the receiver hooks to properly handle deposits. Decentralized applications can take advantage of
224 | signature-based operations to improve user experience dramatically.
225 |
226 | The investment in migration pays dividends through improved security (no more lost tokens due to user error), better
227 | user experience (gasless approvals and single-transaction operations), enhanced functionality (receiver hooks enable
228 | automatic processing), and future-proofing (the protocol's design accommodates future innovations).
229 |
230 | ## Conclusion
231 |
232 | The OP20 standard represents a thoughtful evolution in token design, addressing real problems that have cost users
233 | millions of dollars while enabling new interaction patterns that weren't possible with ERC20. By learning from the
234 | successes and failures of previous standards, OP20 provides a more secure, efficient, and user-friendly foundation for
235 | the next generation of blockchain tokens.
236 |
237 | The standard's emphasis on safety without sacrificing functionality makes it particularly suitable for high-value
238 | applications where user protection is paramount. The gasless signature operations reduce friction for users while
239 | maintaining security, and the comprehensive validation prevents costly mistakes.
240 |
241 | As blockchain technology matures, standards like OP20 demonstrate that we can have both innovation and safety,
242 | efficiency and security, simplicity and sophistication. Whether you're a developer building the next breakthrough
243 | application or a user simply wanting to transf
244 |
--------------------------------------------------------------------------------
/src/stablecoin/MyStableCoin.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from '@btc-vision/as-bignum/assembly';
2 | import {
3 | Address,
4 | Blockchain,
5 | BytesWriter,
6 | Calldata,
7 | OP20InitParameters,
8 | OP20S,
9 | Revert,
10 | Selector,
11 | StoredBoolean,
12 | } from '@btc-vision/btc-runtime/runtime';
13 |
14 | import {
15 | BlacklistedEvent,
16 | BlacklisterChangedEvent,
17 | MinterChangedEvent,
18 | OwnershipTransferredEvent,
19 | OwnershipTransferStartedEvent,
20 | PausedEvent,
21 | PauserChangedEvent,
22 | UnblacklistedEvent,
23 | UnpausedEvent,
24 | } from './events/StableCoinEvents';
25 | import { AddressMemoryMap } from '@btc-vision/btc-runtime/runtime/memory/AddressMemoryMap';
26 |
27 | export const IS_BLACKLISTED_SELECTOR: u32 = 0xd20d08bb;
28 | export const IS_PAUSED_SELECTOR: u32 = 0xe57e24b7;
29 |
30 | const ownerPointer: u16 = Blockchain.nextPointer;
31 | const pendingOwnerPointer: u16 = Blockchain.nextPointer;
32 | const minterPointer: u16 = Blockchain.nextPointer;
33 | const blacklisterPointer: u16 = Blockchain.nextPointer;
34 | const pauserPointer: u16 = Blockchain.nextPointer;
35 | const pausedPointer: u16 = Blockchain.nextPointer;
36 | const blacklistMapPointer: u16 = Blockchain.nextPointer;
37 |
38 | @final
39 | export class MyStableCoin extends OP20S {
40 | private readonly _ownerMap: AddressMemoryMap;
41 | private readonly _pendingOwnerMap: AddressMemoryMap;
42 | private readonly _minterMap: AddressMemoryMap;
43 | private readonly _blacklisterMap: AddressMemoryMap;
44 | private readonly _pauserMap: AddressMemoryMap;
45 | private readonly _paused: StoredBoolean;
46 | private readonly _blacklist: AddressMemoryMap;
47 |
48 | public constructor() {
49 | super();
50 | this._ownerMap = new AddressMemoryMap(ownerPointer);
51 | this._pendingOwnerMap = new AddressMemoryMap(pendingOwnerPointer);
52 | this._minterMap = new AddressMemoryMap(minterPointer);
53 | this._blacklisterMap = new AddressMemoryMap(blacklisterPointer);
54 | this._pauserMap = new AddressMemoryMap(pauserPointer);
55 | this._paused = new StoredBoolean(pausedPointer, false);
56 | this._blacklist = new AddressMemoryMap(blacklistMapPointer);
57 | }
58 |
59 | public override onDeployment(calldata: Calldata): void {
60 | const owner = calldata.readAddress();
61 | const minter = calldata.readAddress();
62 | const blacklister = calldata.readAddress();
63 | const pauser = calldata.readAddress();
64 | const pegAuthority = calldata.readAddress();
65 | const initialPegRate = calldata.readU256();
66 |
67 | this._validateAddress(owner, 'Invalid owner');
68 | this._validateAddress(minter, 'Invalid minter');
69 | this._validateAddress(blacklister, 'Invalid blacklister');
70 | this._validateAddress(pauser, 'Invalid pauser');
71 | this._validateAddress(pegAuthority, 'Invalid peg authority');
72 |
73 | if (initialPegRate.isZero()) {
74 | throw new Revert('Invalid peg rate');
75 | }
76 |
77 | const maxSupply: u256 = u256.Max;
78 | const decimals: u8 = 6;
79 | const name: string = 'Stable USD';
80 | const symbol: string = 'SUSD';
81 |
82 | this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
83 | this.initializePeg(pegAuthority, initialPegRate, 144);
84 |
85 | this._setOwner(owner);
86 | this._setMinter(minter);
87 | this._setBlacklister(blacklister);
88 | this._setPauser(pauser);
89 |
90 | this.emitEvent(new OwnershipTransferredEvent(Address.zero(), owner));
91 | this.emitEvent(new MinterChangedEvent(Address.zero(), minter));
92 | this.emitEvent(new BlacklisterChangedEvent(Address.zero(), blacklister));
93 | this.emitEvent(new PauserChangedEvent(Address.zero(), pauser));
94 | }
95 |
96 | @method(
97 | { name: 'to', type: ABIDataTypes.ADDRESS },
98 | { name: 'amount', type: ABIDataTypes.UINT256 },
99 | )
100 | @emit('Minted')
101 | public mint(calldata: Calldata): BytesWriter {
102 | this._onlyMinter();
103 | this._requireNotPaused();
104 |
105 | const to = calldata.readAddress();
106 | const amount = calldata.readU256();
107 |
108 | this._validateAddress(to, 'Invalid recipient');
109 | this._requireNotBlacklisted(to);
110 |
111 | if (amount.isZero()) {
112 | throw new Revert('Amount is zero');
113 | }
114 |
115 | this._mint(to, amount);
116 |
117 | return new BytesWriter(0);
118 | }
119 |
120 | @method(
121 | { name: 'from', type: ABIDataTypes.ADDRESS },
122 | { name: 'amount', type: ABIDataTypes.UINT256 },
123 | )
124 | @emit('Burned')
125 | public burnFrom(calldata: Calldata): BytesWriter {
126 | this._onlyMinter();
127 | this._requireNotPaused();
128 |
129 | const from = calldata.readAddress();
130 | const amount = calldata.readU256();
131 |
132 | this._validateAddress(from, 'Invalid address');
133 |
134 | const balance = this._balanceOf(from);
135 | if (balance < amount) {
136 | throw new Revert('Insufficient balance');
137 | }
138 |
139 | this._burn(from, amount);
140 |
141 | return new BytesWriter(0);
142 | }
143 |
144 | @method({ name: 'account', type: ABIDataTypes.ADDRESS })
145 | @emit('Blacklisted')
146 | public blacklist(calldata: Calldata): BytesWriter {
147 | this._onlyBlacklister();
148 |
149 | const account = calldata.readAddress();
150 | this._validateAddress(account, 'Invalid address');
151 |
152 | if (this._isBlacklisted(account)) {
153 | throw new Revert('Already blacklisted');
154 | }
155 |
156 | this._blacklist.set(account, u256.One);
157 |
158 | this.emitEvent(new BlacklistedEvent(account, Blockchain.tx.sender));
159 |
160 | return new BytesWriter(0);
161 | }
162 |
163 | @method({ name: 'account', type: ABIDataTypes.ADDRESS })
164 | @emit('Unblacklisted')
165 | public unblacklist(calldata: Calldata): BytesWriter {
166 | this._onlyBlacklister();
167 |
168 | const account = calldata.readAddress();
169 | this._validateAddress(account, 'Invalid address');
170 |
171 | if (!this._isBlacklisted(account)) {
172 | throw new Revert('Not blacklisted');
173 | }
174 |
175 | this._blacklist.set(account, u256.Zero);
176 |
177 | this.emitEvent(new UnblacklistedEvent(account, Blockchain.tx.sender));
178 |
179 | return new BytesWriter(0);
180 | }
181 |
182 | @method({ name: 'account', type: ABIDataTypes.ADDRESS })
183 | @returns({ name: 'blacklisted', type: ABIDataTypes.BOOL })
184 | public isBlacklisted(calldata: Calldata): BytesWriter {
185 | const account = calldata.readAddress();
186 | const w = new BytesWriter(1);
187 | w.writeBoolean(this._isBlacklisted(account));
188 | return w;
189 | }
190 |
191 | @method()
192 | @emit('Paused')
193 | public pause(_: Calldata): BytesWriter {
194 | this._onlyPauser();
195 |
196 | if (this._paused.value) {
197 | throw new Revert('Already paused');
198 | }
199 |
200 | this._paused.value = true;
201 |
202 | this.emitEvent(new PausedEvent(Blockchain.tx.sender));
203 |
204 | return new BytesWriter(0);
205 | }
206 |
207 | @method()
208 | @emit('Unpaused')
209 | public unpause(_: Calldata): BytesWriter {
210 | this._onlyPauser();
211 |
212 | if (!this._paused.value) {
213 | throw new Revert('Not paused');
214 | }
215 |
216 | this._paused.value = false;
217 |
218 | this.emitEvent(new UnpausedEvent(Blockchain.tx.sender));
219 |
220 | return new BytesWriter(0);
221 | }
222 |
223 | @method()
224 | @returns({ name: 'paused', type: ABIDataTypes.BOOL })
225 | public isPaused(_: Calldata): BytesWriter {
226 | const w = new BytesWriter(1);
227 | w.writeBoolean(this._paused.value);
228 | return w;
229 | }
230 |
231 | @method({ name: 'newOwner', type: ABIDataTypes.ADDRESS })
232 | @emit('OwnershipTransferStarted')
233 | public transferOwnership(calldata: Calldata): BytesWriter {
234 | this._onlyOwner();
235 |
236 | const newOwner = calldata.readAddress();
237 | this._validateAddress(newOwner, 'Invalid new owner');
238 |
239 | const currentOwner = this._getOwner();
240 | this._setPendingOwner(newOwner);
241 |
242 | this.emitEvent(new OwnershipTransferStartedEvent(currentOwner, newOwner));
243 |
244 | return new BytesWriter(0);
245 | }
246 |
247 | @method()
248 | @emit('OwnershipTransferred')
249 | public acceptOwnership(_: Calldata): BytesWriter {
250 | const pending = this._getPendingOwner();
251 | if (pending.equals(Address.zero())) {
252 | throw new Revert('No pending owner');
253 | }
254 | if (!Blockchain.tx.sender.equals(pending)) {
255 | throw new Revert('Not pending owner');
256 | }
257 |
258 | const previousOwner = this._getOwner();
259 | this._setOwner(pending);
260 | this._setPendingOwner(Address.zero());
261 |
262 | this.emitEvent(new OwnershipTransferredEvent(previousOwner, pending));
263 |
264 | return new BytesWriter(0);
265 | }
266 |
267 | @method({ name: 'newMinter', type: ABIDataTypes.ADDRESS })
268 | @emit('MinterChanged')
269 | public setMinter(calldata: Calldata): BytesWriter {
270 | this._onlyOwner();
271 |
272 | const newMinter = calldata.readAddress();
273 | this._validateAddress(newMinter, 'Invalid minter');
274 |
275 | const previousMinter = this._getMinter();
276 | this._setMinter(newMinter);
277 |
278 | this.emitEvent(new MinterChangedEvent(previousMinter, newMinter));
279 |
280 | return new BytesWriter(0);
281 | }
282 |
283 | @method({ name: 'newBlacklister', type: ABIDataTypes.ADDRESS })
284 | @emit('BlacklisterChanged')
285 | public setBlacklister(calldata: Calldata): BytesWriter {
286 | this._onlyOwner();
287 |
288 | const newBlacklister = calldata.readAddress();
289 | this._validateAddress(newBlacklister, 'Invalid blacklister');
290 |
291 | const previousBlacklister = this._getBlacklister();
292 | this._setBlacklister(newBlacklister);
293 |
294 | this.emitEvent(new BlacklisterChangedEvent(previousBlacklister, newBlacklister));
295 |
296 | return new BytesWriter(0);
297 | }
298 |
299 | @method({ name: 'newPauser', type: ABIDataTypes.ADDRESS })
300 | @emit('PauserChanged')
301 | public setPauser(calldata: Calldata): BytesWriter {
302 | this._onlyOwner();
303 |
304 | const newPauser = calldata.readAddress();
305 | this._validateAddress(newPauser, 'Invalid pauser');
306 |
307 | const previousPauser = this._getPauser();
308 | this._setPauser(newPauser);
309 |
310 | this.emitEvent(new PauserChangedEvent(previousPauser, newPauser));
311 |
312 | return new BytesWriter(0);
313 | }
314 |
315 | @method()
316 | @returns({ name: 'owner', type: ABIDataTypes.ADDRESS })
317 | public owner(_: Calldata): BytesWriter {
318 | const w = new BytesWriter(32);
319 | w.writeAddress(this._getOwner());
320 | return w;
321 | }
322 |
323 | @method()
324 | @returns({ name: 'minter', type: ABIDataTypes.ADDRESS })
325 | public minter(_: Calldata): BytesWriter {
326 | const w = new BytesWriter(32);
327 | w.writeAddress(this._getMinter());
328 | return w;
329 | }
330 |
331 | @method()
332 | @returns({ name: 'blacklister', type: ABIDataTypes.ADDRESS })
333 | public blacklister(_: Calldata): BytesWriter {
334 | const w = new BytesWriter(32);
335 | w.writeAddress(this._getBlacklister());
336 | return w;
337 | }
338 |
339 | @method()
340 | @returns({ name: 'pauser', type: ABIDataTypes.ADDRESS })
341 | public pauser(_: Calldata): BytesWriter {
342 | const w = new BytesWriter(32);
343 | w.writeAddress(this._getPauser());
344 | return w;
345 | }
346 |
347 | protected override _transfer(from: Address, to: Address, amount: u256): void {
348 | this._requireNotPaused();
349 | this._requireNotBlacklisted(from);
350 | this._requireNotBlacklisted(to);
351 | this._requireNotBlacklisted(Blockchain.tx.sender);
352 |
353 | super._transfer(from, to, amount);
354 | }
355 |
356 | protected override _increaseAllowance(owner: Address, spender: Address, amount: u256): void {
357 | this._requireNotPaused();
358 | this._requireNotBlacklisted(owner);
359 | this._requireNotBlacklisted(spender);
360 |
361 | super._increaseAllowance(owner, spender, amount);
362 | }
363 |
364 | protected override _decreaseAllowance(owner: Address, spender: Address, amount: u256): void {
365 | this._requireNotPaused();
366 | this._requireNotBlacklisted(owner);
367 | this._requireNotBlacklisted(spender);
368 |
369 | super._decreaseAllowance(owner, spender, amount);
370 | }
371 |
372 | protected override isSelectorExcluded(selector: Selector): boolean {
373 | if (selector == IS_BLACKLISTED_SELECTOR || selector == IS_PAUSED_SELECTOR) {
374 | return true;
375 | }
376 | return super.isSelectorExcluded(selector);
377 | }
378 |
379 | private _validateAddress(addr: Address, message: string): void {
380 | if (addr.equals(Address.zero())) {
381 | throw new Revert(message);
382 | }
383 | }
384 |
385 | private _isBlacklisted(account: Address): boolean {
386 | return !this._blacklist.get(account).isZero();
387 | }
388 |
389 | private _requireNotBlacklisted(account: Address): void {
390 | if (this._isBlacklisted(account)) {
391 | throw new Revert('Blacklisted');
392 | }
393 | }
394 |
395 | private _requireNotPaused(): void {
396 | if (this._paused.value) {
397 | throw new Revert('Paused');
398 | }
399 | }
400 |
401 | private _onlyOwner(): void {
402 | if (!Blockchain.tx.sender.equals(this._getOwner())) {
403 | throw new Revert('Not owner');
404 | }
405 | }
406 |
407 | private _onlyMinter(): void {
408 | if (!Blockchain.tx.sender.equals(this._getMinter())) {
409 | throw new Revert('Not minter');
410 | }
411 | }
412 |
413 | private _onlyBlacklister(): void {
414 | if (!Blockchain.tx.sender.equals(this._getBlacklister())) {
415 | throw new Revert('Not blacklister');
416 | }
417 | }
418 |
419 | private _onlyPauser(): void {
420 | if (!Blockchain.tx.sender.equals(this._getPauser())) {
421 | throw new Revert('Not pauser');
422 | }
423 | }
424 |
425 | private _getOwner(): Address {
426 | const stored = this._ownerMap.get(Address.zero());
427 | if (stored.isZero()) return Address.zero();
428 | return this._u256ToAddress(stored);
429 | }
430 |
431 | private _setOwner(addr: Address): void {
432 | this._ownerMap.set(Address.zero(), this._addressToU256(addr));
433 | }
434 |
435 | private _getPendingOwner(): Address {
436 | const stored = this._pendingOwnerMap.get(Address.zero());
437 | if (stored.isZero()) return Address.zero();
438 | return this._u256ToAddress(stored);
439 | }
440 |
441 | private _setPendingOwner(addr: Address): void {
442 | this._pendingOwnerMap.set(Address.zero(), this._addressToU256(addr));
443 | }
444 |
445 | private _getMinter(): Address {
446 | const stored = this._minterMap.get(Address.zero());
447 | if (stored.isZero()) return Address.zero();
448 | return this._u256ToAddress(stored);
449 | }
450 |
451 | private _setMinter(addr: Address): void {
452 | this._minterMap.set(Address.zero(), this._addressToU256(addr));
453 | }
454 |
455 | private _getBlacklister(): Address {
456 | const stored = this._blacklisterMap.get(Address.zero());
457 | if (stored.isZero()) return Address.zero();
458 | return this._u256ToAddress(stored);
459 | }
460 |
461 | private _setBlacklister(addr: Address): void {
462 | this._blacklisterMap.set(Address.zero(), this._addressToU256(addr));
463 | }
464 |
465 | private _getPauser(): Address {
466 | const stored = this._pauserMap.get(Address.zero());
467 | if (stored.isZero()) return Address.zero();
468 | return this._u256ToAddress(stored);
469 | }
470 |
471 | private _setPauser(addr: Address): void {
472 | this._pauserMap.set(Address.zero(), this._addressToU256(addr));
473 | }
474 | }
475 |
--------------------------------------------------------------------------------
/src/nft/MyNFT.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from '@btc-vision/as-bignum/assembly';
2 | import {
3 | Address,
4 | Blockchain,
5 | BytesWriter,
6 | Calldata,
7 | EMPTY_POINTER,
8 | OP721,
9 | OP721InitParameters,
10 | Potential,
11 | Revert,
12 | SafeMath,
13 | StoredBoolean,
14 | StoredMapU256,
15 | StoredString,
16 | StoredU256,
17 | StoredU64Array,
18 | TransactionOutput,
19 | U256_BYTE_LENGTH,
20 | U32_BYTE_LENGTH,
21 | U64_BYTE_LENGTH,
22 | } from '@btc-vision/btc-runtime/runtime';
23 | import {
24 | MintStatusChangedEvent,
25 | ReservationClaimedEvent,
26 | ReservationCreatedEvent,
27 | ReservationExpiredEvent,
28 | } from './events/Reserved';
29 |
30 | @final
31 | class PurgeResult {
32 | constructor(
33 | public totalPurged: u256,
34 | public blocksProcessed: u32,
35 | ) {}
36 | }
37 |
38 | const treasuryAddressPointer: u16 = Blockchain.nextPointer;
39 | const reservationBlockPointer: u16 = Blockchain.nextPointer;
40 | const reservationAmountPointer: u16 = Blockchain.nextPointer;
41 | const blockReservedAmountPointer: u16 = Blockchain.nextPointer;
42 | const totalActiveReservedPointer: u16 = Blockchain.nextPointer;
43 | const blocksWithReservationsPointer: u16 = Blockchain.nextPointer;
44 | const mintEnabledPointer: u16 = Blockchain.nextPointer;
45 |
46 | @final
47 | export class MyNFT extends OP721 {
48 | // Constants
49 | private static readonly MINT_PRICE: u64 = 100000; // 0.001 BTC per NFT
50 | private static readonly RESERVATION_FEE_PERCENT: u64 = 15; // 15% upfront
51 | private static readonly MIN_RESERVATION_FEE: u64 = 1000; // Minimum 1000 sats
52 | private static readonly RESERVATION_BLOCKS: u64 = 5; // 5 blocks to pay
53 | private static readonly GRACE_BLOCKS: u64 = 1; // 1 block grace period
54 | private static readonly MAX_RESERVATION_AMOUNT: u32 = 20; // Max per reservation
55 | private static readonly MAX_BLOCKS_TO_PURGE: u32 = 10; // Max blocks per purge
56 |
57 | private readonly treasuryAddress: StoredString;
58 | private readonly mintEnabled: StoredBoolean;
59 |
60 | // User reservations
61 | private userReservationBlock: StoredMapU256; // address -> block number when reserved
62 | private userReservationAmount: StoredMapU256; // address -> amount reserved
63 |
64 | // Block tracking
65 | private blockReservedAmount: StoredMapU256; // block number -> total reserved in that block
66 | private totalActiveReserved: StoredU256; // Global active reservations counter
67 |
68 | public constructor() {
69 | super();
70 |
71 | this.userReservationBlock = new StoredMapU256(reservationBlockPointer);
72 | this.userReservationAmount = new StoredMapU256(reservationAmountPointer);
73 | this.blockReservedAmount = new StoredMapU256(blockReservedAmountPointer);
74 | this.totalActiveReserved = new StoredU256(totalActiveReservedPointer, EMPTY_POINTER);
75 |
76 | this.treasuryAddress = new StoredString(treasuryAddressPointer);
77 | this.mintEnabled = new StoredBoolean(mintEnabledPointer, false);
78 | }
79 |
80 | private _blocksWithReservations: Potential = null; // Sorted list of blocks with reservations
81 |
82 | public get blocksWithReservations(): StoredU64Array {
83 | if (this._blocksWithReservations === null) {
84 | this._blocksWithReservations = new StoredU64Array(
85 | blocksWithReservationsPointer,
86 | EMPTY_POINTER,
87 | );
88 | }
89 |
90 | return this._blocksWithReservations as StoredU64Array;
91 | }
92 |
93 | public override onDeployment(_calldata: Calldata): void {
94 | const maxSupply: u256 = u256.fromU32(10000);
95 |
96 | // Validate max supply against current state
97 | if (this._totalSupply.value >= maxSupply) {
98 | throw new Revert('Max supply already reached');
99 | }
100 |
101 | const name: string = 'Cool NFT';
102 | const symbol: string = 'O_o';
103 |
104 | const baseURI: string = '';
105 |
106 | // Should be 1500x500-1500x300
107 | const collectionBanner: string =
108 | 'https://raw.githubusercontent.com/btc-vision/contract-logo/refs/heads/main/nft/demo_banner.jpg';
109 |
110 | const collectionIcon: string =
111 | 'https://raw.githubusercontent.com/btc-vision/contract-logo/refs/heads/main/nft/icon.png';
112 |
113 | const collectionWebsite: string = 'https://example.com';
114 | const collectionDescription: string = 'This NFT collection is awesome! 😎';
115 |
116 | this.instantiate(
117 | new OP721InitParameters(
118 | name,
119 | symbol,
120 | baseURI,
121 | maxSupply,
122 | collectionBanner,
123 | collectionIcon,
124 | collectionWebsite,
125 | collectionDescription,
126 | ),
127 | );
128 |
129 | this.treasuryAddress.value = Blockchain.tx.origin.p2tr();
130 | this.mintEnabled.value = false; // Start with minting disabled
131 | }
132 |
133 | @method({ name: 'enabled', type: ABIDataTypes.BOOL })
134 | @emit('MintStatusChanged')
135 | public setMintEnabled(calldata: Calldata): BytesWriter {
136 | this.onlyDeployer(Blockchain.tx.sender);
137 |
138 | const enabled: boolean = calldata.readBoolean();
139 | this.mintEnabled.value = enabled;
140 |
141 | // Emit event for transparency
142 | this.emitEvent(new MintStatusChangedEvent(enabled));
143 |
144 | return new BytesWriter(0);
145 | }
146 |
147 | @method()
148 | @returns({ name: 'enabled', type: ABIDataTypes.BOOL })
149 | public isMintEnabled(_: Calldata): BytesWriter {
150 | const response: BytesWriter = new BytesWriter(1);
151 | response.writeBoolean(this.mintEnabled.value);
152 | return response;
153 | }
154 |
155 | @method(
156 | {
157 | name: 'addresses',
158 | type: ABIDataTypes.ARRAY_OF_ADDRESSES,
159 | },
160 | {
161 | name: 'amounts',
162 | type: ABIDataTypes.ARRAY_OF_UINT8,
163 | },
164 | )
165 | @emit('Transferred')
166 | public airdrop(calldata: Calldata): BytesWriter {
167 | this.onlyDeployer(Blockchain.tx.sender);
168 |
169 | const addresses: Address[] = calldata.readAddressArray();
170 | const amounts: u8[] = calldata.readU8Array();
171 |
172 | if (addresses.length !== amounts.length || addresses.length === 0) {
173 | throw new Revert('Mismatched or empty arrays');
174 | }
175 |
176 | let totalToMint: u32 = 0;
177 |
178 | const addressLength: u32 = u32(addresses.length);
179 | for (let i: u32 = 0; i < addressLength; i++) {
180 | totalToMint += amounts[i];
181 | }
182 |
183 | if (totalToMint === 0) {
184 | throw new Revert('Total mint amount is zero');
185 | }
186 |
187 | // Check supply availability
188 | const currentSupply: u256 = this._totalSupply.value;
189 | const available: u256 = SafeMath.sub(
190 | this.maxSupply,
191 | SafeMath.add(currentSupply, this.totalActiveReserved.value),
192 | );
193 |
194 | if (u256.fromU32(totalToMint) > available) {
195 | throw new Revert('Insufficient supply available');
196 | }
197 |
198 | // Mint NFTs
199 | const startTokenId: u256 = this._nextTokenId.value;
200 | let mintedSoFar: u32 = 0;
201 |
202 | for (let i: u32 = 0; i < addressLength; i++) {
203 | const addr: Address = addresses[i];
204 | const amount: u32 = amounts[i];
205 |
206 | if (amount === 0) continue;
207 |
208 | for (let j: u32 = 0; j < amount; j++) {
209 | const tokenId: u256 = SafeMath.add(startTokenId, u256.fromU32(mintedSoFar));
210 | this._mint(addr, tokenId);
211 | mintedSoFar++;
212 | }
213 | }
214 |
215 | this._nextTokenId.value = SafeMath.add(startTokenId, u256.fromU32(mintedSoFar));
216 |
217 | return new BytesWriter(0);
218 | }
219 |
220 | /**
221 | * @notice Reserve NFTs by paying 15% upfront fee (minimum 1000 sats total)
222 | * @dev Reservations expire after RESERVATION_BLOCKS + GRACE_BLOCKS (6 blocks total)
223 | * @param calldata
224 | */
225 | @method({
226 | name: 'quantity',
227 | type: ABIDataTypes.UINT256,
228 | })
229 | @emit('ReservationCreated')
230 | @returns(
231 | { name: 'remainingPayment', type: ABIDataTypes.UINT64 },
232 | { name: 'reservationBlock', type: ABIDataTypes.UINT64 },
233 | )
234 | public reserve(calldata: Calldata): BytesWriter {
235 | // Check if minting is enabled
236 | if (!this.mintEnabled.value) {
237 | throw new Revert('Minting is disabled');
238 | }
239 |
240 | // Auto-purge expired reservations first
241 | this.autoPurgeExpired();
242 |
243 | const quantity: u256 = calldata.readU256();
244 | const sender: Address = Blockchain.tx.sender;
245 |
246 | if (quantity.isZero() || quantity > u256.fromU32(MyNFT.MAX_RESERVATION_AMOUNT)) {
247 | throw new Revert('Invalid quantity: 1-20 only');
248 | }
249 |
250 | const senderKey: u256 = this._u256FromAddress(sender);
251 |
252 | // Check if user has existing reservation (expired or not)
253 | const existingBlock: u256 = this.userReservationBlock.get(senderKey);
254 |
255 | if (!existingBlock.isZero()) {
256 | const currentBlock: u64 = Blockchain.block.number;
257 | const totalExpiry: u64 = SafeMath.add64(
258 | SafeMath.add64(existingBlock.toU64(), MyNFT.RESERVATION_BLOCKS),
259 | MyNFT.GRACE_BLOCKS,
260 | );
261 |
262 | if (currentBlock <= totalExpiry) {
263 | throw new Revert('Active reservation exists');
264 | }
265 | // If expired, we just overwrite it below - no cleanup
266 | }
267 |
268 | const qty: u64 = quantity.toU64();
269 | const totalCost: u64 = SafeMath.mul64(MyNFT.MINT_PRICE, qty);
270 | const calculatedFee: u64 = SafeMath.div64(
271 | SafeMath.mul64(totalCost, MyNFT.RESERVATION_FEE_PERCENT),
272 | 100,
273 | );
274 |
275 | // Apply minimum fee of 1000 sats
276 | const reservationFee: u64 =
277 | calculatedFee < MyNFT.MIN_RESERVATION_FEE ? MyNFT.MIN_RESERVATION_FEE : calculatedFee;
278 |
279 | // Validate payment
280 | if (!this.validatePayment(reservationFee)) {
281 | throw new Revert('Insufficient reservation fee');
282 | }
283 |
284 | // Check supply availability
285 | const currentSupply: u256 = this._totalSupply.value;
286 | const available: u256 = SafeMath.sub(
287 | this.maxSupply,
288 | SafeMath.add(currentSupply, this.totalActiveReserved.value),
289 | );
290 |
291 | if (quantity > available) {
292 | throw new Revert('Insufficient supply available');
293 | }
294 |
295 | // Store reservation (overwrite any expired data)
296 | const currentBlock: u256 = u256.fromU64(Blockchain.block.number);
297 | this.userReservationBlock.set(senderKey, currentBlock);
298 | this.userReservationAmount.set(senderKey, quantity);
299 |
300 | // Update block reserved amount
301 | const currentBlockReserved: u256 = this.blockReservedAmount.get(currentBlock);
302 | this.blockReservedAmount.set(currentBlock, SafeMath.add(currentBlockReserved, quantity));
303 |
304 | // Track this block if new
305 | this.trackBlockWithReservation(Blockchain.block.number);
306 |
307 | // Update total active reserved
308 | this.totalActiveReserved.value = SafeMath.add(this.totalActiveReserved.value, quantity);
309 |
310 | // Emit event
311 | this.emitEvent(
312 | new ReservationCreatedEvent(sender, quantity, Blockchain.block.number, reservationFee),
313 | );
314 |
315 | const remainingPayment: u64 = SafeMath.sub64(totalCost, reservationFee);
316 | const response: BytesWriter = new BytesWriter(8 + U256_BYTE_LENGTH);
317 | response.writeU64(remainingPayment);
318 | response.writeU64(currentBlock.toU64());
319 |
320 | return response;
321 | }
322 |
323 | /**
324 | * @notice Claims reserved NFTs by paying the remaining balance
325 | * @dev Must be called within RESERVATION_BLOCKS + GRACE_BLOCKS (6 blocks total)
326 | * Block 0: reservation created
327 | * Blocks 1-5: standard claim period
328 | * Block 6: grace period (last chance to claim)
329 | * Block 7+: expired, reservation can be purged
330 | */
331 | @method()
332 | @emit('ReservationClaimed')
333 | @emit('Transferred')
334 | public claim(_: Calldata): BytesWriter {
335 | const sender: Address = Blockchain.tx.sender;
336 | const senderKey: u256 = this._u256FromAddress(sender);
337 |
338 | // Get reservation
339 | const reservedBlock: u256 = this.userReservationBlock.get(senderKey);
340 | const reservedAmount: u256 = this.userReservationAmount.get(senderKey);
341 |
342 | if (reservedBlock.isZero() || reservedAmount.isZero()) {
343 | throw new Revert('No reservation found');
344 | }
345 |
346 | // Check expiry INCLUDING grace period
347 | const currentBlock: u64 = Blockchain.block.number;
348 | const claimDeadline: u64 = SafeMath.add64(
349 | SafeMath.add64(reservedBlock.toU64(), MyNFT.RESERVATION_BLOCKS),
350 | MyNFT.GRACE_BLOCKS,
351 | );
352 |
353 | if (currentBlock > claimDeadline) {
354 | throw new Revert('Reservation expired');
355 | }
356 |
357 | // Calculate exact payment needed with SafeMath
358 | const qty: u64 = reservedAmount.toU64();
359 | const totalCost: u64 = SafeMath.mul64(MyNFT.MINT_PRICE, qty);
360 | const calculatedFee: u64 = SafeMath.div64(
361 | SafeMath.mul64(totalCost, MyNFT.RESERVATION_FEE_PERCENT),
362 | 100,
363 | );
364 | const alreadyPaid: u64 =
365 | calculatedFee < MyNFT.MIN_RESERVATION_FEE ? MyNFT.MIN_RESERVATION_FEE : calculatedFee;
366 | const exactPaymentNeeded: u64 = SafeMath.sub64(totalCost, alreadyPaid);
367 |
368 | // Validate EXACT payment (or more)
369 | const paymentReceived: u64 = this.getExactPayment();
370 | if (paymentReceived < exactPaymentNeeded) {
371 | throw new Revert('Insufficient payment - funds lost');
372 | }
373 |
374 | // Mint NFTs
375 | const startTokenId: u256 = this._nextTokenId.value;
376 | const amountToMint: u32 = reservedAmount.toU32();
377 | for (let i: u32 = 0; i < amountToMint; i++) {
378 | const tokenId: u256 = SafeMath.add(startTokenId, u256.fromU32(i));
379 | this._mint(sender, tokenId);
380 | }
381 | this._nextTokenId.value = SafeMath.add(startTokenId, reservedAmount);
382 |
383 | // Reduce block reserved amount
384 | const blockReserved: u256 = this.blockReservedAmount.get(reservedBlock);
385 | const newBlockReserved: u256 = SafeMath.sub(blockReserved, reservedAmount);
386 | this.blockReservedAmount.set(reservedBlock, newBlockReserved);
387 |
388 | // Update total and clear user reservation
389 | this.totalActiveReserved.value = SafeMath.sub(
390 | this.totalActiveReserved.value,
391 | reservedAmount,
392 | );
393 | this.userReservationBlock.set(senderKey, u256.Zero);
394 | this.userReservationAmount.set(senderKey, u256.Zero);
395 |
396 | // Emit event
397 | this.emitEvent(new ReservationClaimedEvent(sender, reservedAmount, startTokenId));
398 |
399 | const response: BytesWriter = new BytesWriter(U256_BYTE_LENGTH * 2);
400 | response.writeU256(startTokenId);
401 | response.writeU256(reservedAmount);
402 | return response;
403 | }
404 |
405 | @method()
406 | @emit('ReservationExpired')
407 | public purgeExpired(_: Calldata): BytesWriter {
408 | const result: PurgeResult = this.autoPurgeExpired();
409 |
410 | const response: BytesWriter = new BytesWriter(U256_BYTE_LENGTH + U32_BYTE_LENGTH);
411 | response.writeU256(result.totalPurged);
412 | response.writeU32(result.blocksProcessed);
413 | return response;
414 | }
415 |
416 | @method()
417 | @returns(
418 | { name: 'minted', type: ABIDataTypes.UINT256 },
419 | { name: 'reserved', type: ABIDataTypes.UINT256 },
420 | { name: 'available', type: ABIDataTypes.UINT256 },
421 | { name: 'maxSupply', type: ABIDataTypes.UINT256 },
422 | { name: 'blocksWithReservations', type: ABIDataTypes.UINT32 },
423 | { name: 'pricePerToken', type: ABIDataTypes.UINT64 },
424 | { name: 'reservationFeePercent', type: ABIDataTypes.UINT64 },
425 | { name: 'minReservationFee', type: ABIDataTypes.UINT64 },
426 | )
427 | public getStatus(_: Calldata): BytesWriter {
428 | // Auto-purge expired reservations to show accurate available supply
429 | this.autoPurgeExpired();
430 |
431 | const minted: u256 = this._totalSupply.value;
432 | const reserved: u256 = this.totalActiveReserved.value;
433 | const available: u256 = SafeMath.sub(this.maxSupply, SafeMath.add(minted, reserved));
434 |
435 | const response: BytesWriter = new BytesWriter(
436 | U256_BYTE_LENGTH * 4 + U32_BYTE_LENGTH + U64_BYTE_LENGTH * 3,
437 | );
438 | response.writeU256(minted);
439 | response.writeU256(reserved);
440 | response.writeU256(available);
441 | response.writeU256(this.maxSupply);
442 | response.writeU32(this.blocksWithReservations.getLength());
443 | response.writeU64(MyNFT.MINT_PRICE);
444 | response.writeU64(MyNFT.RESERVATION_FEE_PERCENT);
445 | response.writeU64(MyNFT.MIN_RESERVATION_FEE);
446 | return response;
447 | }
448 |
449 | @method(
450 | { name: 'tokenId', type: ABIDataTypes.UINT256 },
451 | { name: 'uri', type: ABIDataTypes.STRING },
452 | )
453 | @emit('URI')
454 | public setTokenURI(calldata: Calldata): BytesWriter {
455 | this.onlyDeployer(Blockchain.tx.sender);
456 |
457 | const tokenId: u256 = calldata.readU256();
458 | const uri: string = calldata.readStringWithLength();
459 |
460 | this._setTokenURI(tokenId, uri);
461 |
462 | return new BytesWriter(0);
463 | }
464 |
465 | private autoPurgeExpired(): PurgeResult {
466 | const cutoffBlock: u64 = SafeMath.sub64(
467 | Blockchain.block.number,
468 | SafeMath.add64(MyNFT.RESERVATION_BLOCKS, MyNFT.GRACE_BLOCKS),
469 | );
470 |
471 | let totalPurged: u256 = u256.Zero;
472 | let blocksProcessed: u32 = 0;
473 |
474 | // Process blocks from the tracked list
475 | while (
476 | this.blocksWithReservations.getLength() > 0 &&
477 | blocksProcessed < MyNFT.MAX_BLOCKS_TO_PURGE
478 | ) {
479 | const oldestBlock: u64 = this.blocksWithReservations.get(0);
480 |
481 | // Stop if we reach blocks that aren't expired yet
482 | if (oldestBlock > cutoffBlock) {
483 | break;
484 | }
485 |
486 | const blockKey: u256 = u256.fromU64(oldestBlock);
487 | const blockReserved: u256 = this.blockReservedAmount.get(blockKey);
488 |
489 | if (!blockReserved.isZero()) {
490 | // Add back to available supply
491 | totalPurged = SafeMath.add(totalPurged, blockReserved);
492 | this.totalActiveReserved.value = SafeMath.sub(
493 | this.totalActiveReserved.value,
494 | blockReserved,
495 | );
496 | this.blockReservedAmount.set(blockKey, u256.Zero);
497 |
498 | // Emit event
499 | this.emitEvent(new ReservationExpiredEvent(oldestBlock, blockReserved));
500 | }
501 |
502 | // Remove this block from tracking
503 | this.blocksWithReservations.shift();
504 | blocksProcessed++;
505 | }
506 |
507 | if (blocksProcessed > 0) {
508 | this.blocksWithReservations.save();
509 | }
510 |
511 | return new PurgeResult(totalPurged, blocksProcessed);
512 | }
513 |
514 | private trackBlockWithReservation(blockNumber: u64): void {
515 | const length: u32 = this.blocksWithReservations.getLength();
516 |
517 | // Only add if not already present (blocks are sorted)
518 | if (length === 0 || this.blocksWithReservations.get(length - 1) !== blockNumber) {
519 | this.blocksWithReservations.push(blockNumber);
520 | this.blocksWithReservations.save();
521 | }
522 | }
523 |
524 | private validatePayment(requiredAmount: u64): boolean {
525 | const totalPaid: u64 = this.getExactPayment();
526 | return totalPaid >= requiredAmount;
527 | }
528 |
529 | private getExactPayment(): u64 {
530 | const outputs: TransactionOutput[] = Blockchain.tx.outputs;
531 | let totalPaid: u64 = 0;
532 |
533 | for (let i: i32 = 0; i < outputs.length; i++) {
534 | const output: TransactionOutput = outputs[i];
535 |
536 | if (!output.to) continue;
537 |
538 | if (output.to === this.treasuryAddress.value) {
539 | totalPaid = SafeMath.add64(totalPaid, output.value);
540 | }
541 | }
542 |
543 | return totalPaid;
544 | }
545 | }
546 |
--------------------------------------------------------------------------------