├── 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 | ![Bitcoin](https://img.shields.io/badge/Bitcoin-000?style=for-the-badge&logo=bitcoin&logoColor=white) 4 | ![AssemblyScript](https://img.shields.io/badge/assembly%20script-%23000000.svg?style=for-the-badge&logo=assemblyscript&logoColor=white) 5 | ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) 6 | ![NodeJS](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white) 7 | ![WebAssembly](https://img.shields.io/badge/WebAssembly-654FF0?style=for-the-badge&logo=webassembly&logoColor=white) 8 | ![NPM](https://img.shields.io/badge/npm-CB3837?style=for-the-badge&logo=npm&logoColor=white) 9 | 10 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](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 | --------------------------------------------------------------------------------