├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ ├── ---documentation.md │ └── ---feature-request.md └── workflows │ └── smart_asa_ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── account.py ├── sandbox.py ├── smart_asa_abi.json ├── smart_asa_approval.teal ├── smart_asa_asc.py ├── smart_asa_clear.teal ├── smart_asa_cli.py ├── smart_asa_client.py ├── smart_asa_test.py └── utils.py /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Bug report on Smart ASA Client, Tests or CLI 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ⚠ If you find a flaw or bug in the Smart Contract, for security reasons we kindly ask you not to disclose it here but rather to report it privately to [Algorand Labs](mailto:dev@algorandlabs.com) 11 | 12 | **Section** 13 | - [ ] Client 14 | - [ ] Tests 15 | - [ ] CLI 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | Steps to reproduce the behavior: 22 | 1. Go to '...' 23 | 2. Click on '....' 24 | 3. Scroll down to '....' 25 | 4. See error 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Current behavior** 31 | A clear and concise description of what you expected to happen. 32 | 33 | **Additional context and environment** 34 | Add any other context about the problem here, e.g. Python version, PyTeal version, etc. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4DA Documentation" 3 | about: Help us improve Smart ASA docs 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | Do you think something is missing or not clear enough in Smart ASA docs? Let us know! 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: Suggest an idea for Smart ASA 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/smart_asa_ci.yaml: -------------------------------------------------------------------------------- 1 | name: Smart ASA Build and Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-and-test: 7 | name: Build & Test Smart ASA 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Install pipenv 14 | run: | 15 | python -m pip install --upgrade pipenv wheel 16 | - name: Set up Python 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: 3.10.4 20 | cache: 'pipenv' 21 | 22 | - name: Install dependencies 23 | run: | 24 | pipenv install --deploy --dev 25 | - uses: pre-commit/action@v2.0.3 26 | name: "Linters and formatters check" 27 | with: 28 | extra_args: --all-files 29 | 30 | - name: Run Algorand Sandbox 31 | run: | 32 | docker run -d -p 4001:4001 -p 4002:4002 matteojug/algorand-sandbox-dev:3.9.2 33 | 34 | - name: Sleep to allow the sandbox to start 35 | run: sleep 10 36 | 37 | - name: Run pytest 38 | run: pipenv run pytest 39 | 40 | - name: Stop running images 41 | run: | 42 | docker stop $(docker ps -q --filter ancestor=matteojug/algorand-sandbox-dev:3.9.2) 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | 4 | # Environments 5 | .env 6 | .venv 7 | env/ 8 | venv/ 9 | 10 | # IDE 11 | .idea/ 12 | 13 | # Debug 14 | dryrun.msgp 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 22.3.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/myint/autoflake 13 | rev: v1.4 14 | hooks: 15 | - id: autoflake 16 | args: 17 | - --in-place 18 | - --remove-unused-variables 19 | - --remove-all-unused-imports 20 | - --expand-star-imports 21 | - --ignore-init-module-imports 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Algorand 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 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | docopt = "*" 8 | py-algorand-sdk = "*" 9 | pyteal = "*" 10 | 11 | [dev-packages] 12 | autoflake = "*" 13 | black = "*" 14 | pytest = "*" 15 | 16 | [requires] 17 | python_version = "3.10" 18 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "9e365e3cfaa4f365ebf2ac92d3fa16f26f2e0935b39f2ab4e04941c830f37056" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "cffi": { 20 | "hashes": [ 21 | "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", 22 | "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", 23 | "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", 24 | "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", 25 | "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", 26 | "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", 27 | "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", 28 | "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", 29 | "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", 30 | "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", 31 | "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", 32 | "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", 33 | "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", 34 | "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", 35 | "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", 36 | "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", 37 | "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", 38 | "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", 39 | "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", 40 | "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", 41 | "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", 42 | "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", 43 | "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", 44 | "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", 45 | "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", 46 | "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", 47 | "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", 48 | "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", 49 | "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", 50 | "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", 51 | "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", 52 | "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", 53 | "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", 54 | "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", 55 | "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", 56 | "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", 57 | "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", 58 | "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", 59 | "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", 60 | "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", 61 | "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", 62 | "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", 63 | "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", 64 | "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", 65 | "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", 66 | "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", 67 | "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", 68 | "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", 69 | "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", 70 | "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", 71 | "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", 72 | "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", 73 | "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", 74 | "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", 75 | "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", 76 | "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", 77 | "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", 78 | "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", 79 | "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", 80 | "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", 81 | "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", 82 | "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", 83 | "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", 84 | "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" 85 | ], 86 | "version": "==1.15.1" 87 | }, 88 | "docopt": { 89 | "hashes": [ 90 | "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" 91 | ], 92 | "index": "pypi", 93 | "version": "==0.6.2" 94 | }, 95 | "docstring-parser": { 96 | "hashes": [ 97 | "sha256:14ac6ec1f1ba6905c4d8cb90fd0bc55394f5678183752c90e44812bf28d7a515", 98 | "sha256:2c77522e31b7c88b1ab457a1f3c9ae38947ad719732260ba77ee8a3deb58622a" 99 | ], 100 | "markers": "python_version >= '3.6' and python_version < '4.0'", 101 | "version": "==0.14.1" 102 | }, 103 | "msgpack": { 104 | "hashes": [ 105 | "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467", 106 | "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae", 107 | "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92", 108 | "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef", 109 | "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624", 110 | "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227", 111 | "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88", 112 | "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9", 113 | "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8", 114 | "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd", 115 | "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6", 116 | "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55", 117 | "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e", 118 | "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2", 119 | "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44", 120 | "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6", 121 | "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9", 122 | "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab", 123 | "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae", 124 | "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa", 125 | "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9", 126 | "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e", 127 | "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250", 128 | "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce", 129 | "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075", 130 | "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236", 131 | "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae", 132 | "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e", 133 | "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f", 134 | "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08", 135 | "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6", 136 | "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d", 137 | "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43", 138 | "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1", 139 | "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6", 140 | "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0", 141 | "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c", 142 | "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff", 143 | "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db", 144 | "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243", 145 | "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661", 146 | "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba", 147 | "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e", 148 | "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb", 149 | "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52", 150 | "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6", 151 | "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1", 152 | "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f", 153 | "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da", 154 | "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f", 155 | "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c", 156 | "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8" 157 | ], 158 | "version": "==1.0.4" 159 | }, 160 | "py-algorand-sdk": { 161 | "hashes": [ 162 | "sha256:37079777a2eb3883f0001e0daa3eacdb7dcc0dbca5ae470ec11bbdf6acd4872f", 163 | "sha256:ee5c999c5209fe5a5be8fd8b5fff16009187fc61cfd4714782e676c985aa163c" 164 | ], 165 | "index": "pypi", 166 | "version": "==1.20.2" 167 | }, 168 | "pycparser": { 169 | "hashes": [ 170 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 171 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 172 | ], 173 | "version": "==2.21" 174 | }, 175 | "pycryptodomex": { 176 | "hashes": [ 177 | "sha256:011e859026ecbd15b8e720e8992361186e582cf726c50bde6ff8c0c05e820ddf", 178 | "sha256:0b42e2743893f386dfb58fe24a4c8be5305c3d1c825d5f23d9e63fd0700d1110", 179 | "sha256:0b7154aff2272962355f8941fd514104a88cb29db2d8f43a29af900d6398eb1c", 180 | "sha256:0bc4b7bfaac56e6dfd62044847443a3d110c7abea7fcb0d68c1aea64ed3a6697", 181 | "sha256:10c2eed4efdfa084b602ab922e699a0a2ba82053baebfc8afcaf27489def7955", 182 | "sha256:1c04cfff163c05d033bf28e3c4429d8222796738c7b6c1638b9d7090b904611e", 183 | "sha256:23707238b024b36c35dd3428f5af6c1f0c5ef54c21e387a2063633717699b8b2", 184 | "sha256:371bbe0be17b4dd8cc0c2f378d75ea33f00d5a39884c09a672016ac40145a5fa", 185 | "sha256:39eb1f82ac3ba3e39d866f38e480e8fa53fcdd22260340f05f54a8188d47d510", 186 | "sha256:3f3c58971784fba0e014bc3f8aed1197b86719631e1b597d36d7354be5598312", 187 | "sha256:5ca98de2e5ac100e57a7116309723360e8f799f722509e376dc396cdf65eec9c", 188 | "sha256:62f51a63d73153482729904381dd2de86800b0733a8814ee8f072fa73e5c92fb", 189 | "sha256:76414d39df6b45bcc4f38cf1ba2031e0f4b8e99d1ba3c2eee31ffe1b9f039733", 190 | "sha256:8dffe067d5fff14dba4d18ff7d459cc2a47576d82dafbff13a8f1199c3353e41", 191 | "sha256:96000b837bcd8e3bf86b419924a056c978e45027281e4318650c81c25a3ef6cc", 192 | "sha256:9919a1edd2a83c4dfb69f1d8a4c0c5efde7147ef15b07775633372b80c90b5d8", 193 | "sha256:aab7941c2ff53eb63cb26252770e4f14386d79ce07baeffbf98a1323c1646545", 194 | "sha256:ac562e239d98cfef763866c0aee4586affb0d58c592202f06c87241af99db241", 195 | "sha256:ae75eea2e908383fd4c659fdcfe9621a72869e3e3ee73904227e93b7f7b80b54", 196 | "sha256:b5c336dc698650283ad06f8c0237a984087d0af9f403ff21d633507335628156", 197 | "sha256:beb5f0664f49b6093da179ee8e27c1d670779f50b9ece0886ce491bb8bd63728", 198 | "sha256:c1ae2fb8d5d6771670436dcc889b293e363c97647a6d31c21eebc12b7b760010", 199 | "sha256:c9332b04bf3f838327087b028f690f4ddb9341eb014a0221e79b9c19a77f7555", 200 | "sha256:c9cb88ed323be1aa642b3c17cd5caa1a03c3a8fbad092d48ecefe88e328ffae3", 201 | "sha256:d45d0d35a238d838b872598fa865bbfb31aaef9aeeda77c68b04ef79f9a469dc", 202 | "sha256:d7a77391fd351ff1bdf8475558ddc6e92950218cb905419ee14aa02f370f1054", 203 | "sha256:de5a43901e47e7a6938490fc5de3074f6e35c8b481a75b227c0d24d6099bd41d", 204 | "sha256:e94a7e986b117b72e9472f8eafdd81748dafff30815401f9760f759f1debe9ef", 205 | "sha256:ed3bdda44cc05dd13eee697ab9bea6928531bb7b218e68e66d0d3eb2ebab043e", 206 | "sha256:f24f49fc6bd706d87048654d6be6c7c967d6836d4879e3a7c439275fab9948ad", 207 | "sha256:f8a97b1acd36e9ce9d4067d94a8be99c458f0eb8070828639302a95cfcf0770b", 208 | "sha256:f8b3d9e7c17c1ffc1fa5b11c0bbab8a5df3de8596bb32ad30281b21e5ede4bf5" 209 | ], 210 | "index": "pypi", 211 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 212 | "version": "==3.19.1" 213 | }, 214 | "pynacl": { 215 | "hashes": [ 216 | "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", 217 | "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", 218 | "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", 219 | "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", 220 | "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", 221 | "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", 222 | "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", 223 | "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", 224 | "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", 225 | "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" 226 | ], 227 | "markers": "python_version >= '3.6'", 228 | "version": "==1.5.0" 229 | }, 230 | "pyteal": { 231 | "hashes": [ 232 | "sha256:75e64c820944f5602b0e16cfef36d4edf77d4c6543de3bc8d368412c5a8d00d3", 233 | "sha256:d1c2125b5af07e3b5d845e8df27084e628b59f1deacda751a8f1ee281a58d7f8" 234 | ], 235 | "index": "pypi", 236 | "version": "==0.20.1" 237 | }, 238 | "semantic-version": { 239 | "hashes": [ 240 | "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", 241 | "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177" 242 | ], 243 | "markers": "python_version >= '2.7'", 244 | "version": "==2.10.0" 245 | } 246 | }, 247 | "develop": { 248 | "attrs": { 249 | "hashes": [ 250 | "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", 251 | "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" 252 | ], 253 | "markers": "python_version >= '3.6'", 254 | "version": "==22.2.0" 255 | }, 256 | "autoflake": { 257 | "hashes": [ 258 | "sha256:7185b596e70d8970c6d4106c112ef41921e472bd26abf3613db99eca88cc8c2a", 259 | "sha256:d58ed4187c6b4f623a942b9a90c43ff84bf6a266f3682f407b42ca52073c9678" 260 | ], 261 | "index": "pypi", 262 | "version": "==2.0.0" 263 | }, 264 | "black": { 265 | "hashes": [ 266 | "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320", 267 | "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351", 268 | "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350", 269 | "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", 270 | "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", 271 | "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148", 272 | "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4", 273 | "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d", 274 | "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc", 275 | "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d", 276 | "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2", 277 | "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f" 278 | ], 279 | "index": "pypi", 280 | "version": "==22.12.0" 281 | }, 282 | "click": { 283 | "hashes": [ 284 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 285 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 286 | ], 287 | "markers": "python_version >= '3.7'", 288 | "version": "==8.1.3" 289 | }, 290 | "exceptiongroup": { 291 | "hashes": [ 292 | "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e", 293 | "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23" 294 | ], 295 | "markers": "python_version < '3.11'", 296 | "version": "==1.1.0" 297 | }, 298 | "iniconfig": { 299 | "hashes": [ 300 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 301 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 302 | ], 303 | "version": "==1.1.1" 304 | }, 305 | "mypy-extensions": { 306 | "hashes": [ 307 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 308 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 309 | ], 310 | "version": "==0.4.3" 311 | }, 312 | "packaging": { 313 | "hashes": [ 314 | "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3", 315 | "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3" 316 | ], 317 | "markers": "python_version >= '3.7'", 318 | "version": "==22.0" 319 | }, 320 | "pathspec": { 321 | "hashes": [ 322 | "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6", 323 | "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6" 324 | ], 325 | "markers": "python_version >= '3.7'", 326 | "version": "==0.10.3" 327 | }, 328 | "platformdirs": { 329 | "hashes": [ 330 | "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", 331 | "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" 332 | ], 333 | "markers": "python_version >= '3.7'", 334 | "version": "==2.6.2" 335 | }, 336 | "pluggy": { 337 | "hashes": [ 338 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 339 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 340 | ], 341 | "markers": "python_version >= '3.6'", 342 | "version": "==1.0.0" 343 | }, 344 | "pyflakes": { 345 | "hashes": [ 346 | "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", 347 | "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" 348 | ], 349 | "markers": "python_version >= '3.6'", 350 | "version": "==3.0.1" 351 | }, 352 | "pytest": { 353 | "hashes": [ 354 | "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", 355 | "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" 356 | ], 357 | "index": "pypi", 358 | "version": "==7.2.0" 359 | }, 360 | "tomli": { 361 | "hashes": [ 362 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 363 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 364 | ], 365 | "markers": "python_version < '3.11'", 366 | "version": "==2.0.1" 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smart-ASA 2 | 3 | Smart ASA reference implementation combines the simplicity and security of the Algorand Standard Assets (ASA)s with the composability and programmability of Algorand Smart Contracts. The Smart ASA offers a new, powerful, L1 feature that extends regular ASAs up to the limits of your imagination! 4 | 5 | - [Smart ASA ABI JSON](https://github.com/algorandlabs/smart-asa/blob/main/smart_asa_abi.json) 6 | - [Smart ASA App TEAL Approval](https://github.com/algorandlabs/smart-asa/blob/main/smart_asa_approval.teal) 7 | - [Smart ASA App TEAL Clear](https://github.com/algorandlabs/smart-asa/blob/main/smart_asa_clear.teal) 8 | - [Smart ASA App Example](https://explorer.dappflow.org/explorer/application/107042851/transactions) 9 | 10 | **⚠️ Disclamer: This code is not audited!** 11 | 12 | - [Overview](#overview) 13 | - [Reference implementation rational](#reference-implementation-rational) 14 | * [Underlying ASA configuration](#underlying-asa-configuration) 15 | * [State Schema](#state-schema) 16 | + [Global State](#global-state) 17 | + [Local State](#local-state) 18 | + [Self Validation](#self-validation) 19 | + [Smart Contract ABI's type check](#smart-contract-abi-s-type-check) 20 | - [Smart ASA Methods](#smart-asa-methods) 21 | * [Smart ASA App Create](#smart-asa-app-create) 22 | * [Smart ASA App Opt-In](#smart-asa-app-opt-in) 23 | * [Smart ASA App Close-Out](#smart-asa-app-close-out) 24 | * [Smart ASA Creation](#smart-asa-creation) 25 | * [Smart ASA Configuration](#smart-asa-configuration) 26 | * [Smart ASA Transfer](#smart-asa-transfer) 27 | + [Mint](#mint) 28 | + [Burn](#burn) 29 | + [Clawback](#clawback) 30 | + [Transfer](#transfer) 31 | * [Smart ASA Global Freeze](#smart-asa-global-freeze) 32 | * [Smart ASA Account Freeze](#smart-asa-account-freeze) 33 | * [Smart ASA Destroy](#smart-asa-destroy) 34 | * [Smart ASA Getters](#smart-asa-getters) 35 | - [Smart ASA CLI](#smart-asa-cli) 36 | * [Install](#install) 37 | * [Usage](#usage) 38 | * [Create Smart ASA NFT](#create-smart-asa-nft) 39 | * [Fractionalize Smart ASA NFT](#fractionalize-smart-asa-nft) 40 | * [Smart ASA NFT opt-in](#smart-asa-nft-opt-in) 41 | * [Mint Smart ASA NFT](#mint-smart-asa-nft) 42 | * [Smart NFT ASA global freeze](#smart-nft-asa-global-freeze) 43 | * [Smart NFT ASA rename](#smart-nft-asa-rename) 44 | * [Smart NFT ASA global unfreeze](#smart-nft-asa-global-unfreeze) 45 | * [Smart NFT ASA burn](#smart-nft-asa-burn) 46 | * [Smart ASA destroy](#smart-asa-destroy) 47 | - [Security Considerations](#security-considerations) 48 | * [Prevent malicious Close-Out and Clear State](#prevent-malicious-close-out-and-clear-state) 49 | * [Conscious Smart ASA Destroy](#conscious-smart-asa-destroy) 50 | - [Conclusions](#conclusions) 51 | 52 | ## Overview 53 | 54 | The Smart ASA introduced with [ARC-0020](https://github.com/aldur/ARCs/blob/smartasa/ARCs/arc-0020.md) represents a new building block for blockchain applications. It offers a more flexible way to work with ASAs providing re-configuration functionalities and the possibility of building additional business logics around operations like asset _transfers_, _royalties_, _role-based-transfers_, _limit-amount-transfers_, _mints_, and _burns_. This reference implementation provides a basic Smart ASA contract as well as an easy to use CLI to interact with its functionalities. 55 | 56 | It is worth noting that Smart ASA extends ASA configurability to **any field**! 57 | With Smart ASA users can now reconfigure properties like `name`, `unit_name`, `url` and even properties like `total`, `decimals` or `default_frozen`! This makes the Smart ASA a perfect fit for "evolvable" assets, like NFTs. 58 | 59 | 60 | ## Reference implementation rational 61 | 62 | A Smart ASA combines together a traditional ASA, called *Underlying ASA*, and an Algorand Smart Contract, called *Smart ASA App*. Essentially, the smart contract controls the ASA, implementing the `create`, `opt-in`, `configure`, `transfer`, `global-freeze`, `freeze`, `close-out`, and `destroy` methods, plus some useful `getters`. 63 | 64 | ### Underlying ASA configuration 65 | 66 | The `create` method triggers an `AssetConfigTx` transaction (inner transaction) that generates a new ASA, called *Underlying ASA*. The ASA is created with the following configuration: 67 | 68 | | Property | Value | 69 | |------------------|---------------------------------------| 70 | | `total` | 2^64-1 | 71 | | `decimals` | 0 | 72 | | `default_frozen` | True | 73 | | `unit_name` | S-ASA | 74 | | `asset_name` | SMART-ASA | 75 | | `url` | smart-asa-app-id:\ | 76 | | `manager_addr` | \ | 77 | | `reserve_addr` | \ | 78 | | `freeze_addr` | \ | 79 | | `clawback_name` | \ | 80 | 81 | The ASA is set with maximum supply as (`total` = max `uint64`), it is not divisible, and it is frozen by default. The unit and asset names are standard strings that identify a Smart ASA. 82 | 83 | The `url` field is used to bind the Underlying ASA with the Smart ASA App ID who controls it, with the following encoding: 84 | 85 | `smart-asa-app-id:` 86 | 87 | Finally, the `manager`, `reserve`, `freeze`, and `clawback` roles are assigned to the *Smart ASA App* address. 88 | 89 | > **The Underlying ASA can only be controlled by the smart contract**. 90 | 91 | ### State Schema 92 | 93 | The `StateSchema` of the Smart Contract has been designed to match 1-to-1 the parameters of an ASA. This reference implementation also requires users to initialize their `LocalState` by opting-in into the application. 94 | 95 | #### Global State 96 | 97 | The `GlobalState` of the Smart ASA App is defined as follows: 98 | 99 | Integer Variables: 100 | 101 | - `total`: total supply of a Smart ASA. This value cannot be greater than the *Underlying ASA* total supply or lower than the current cirulating supply (in case of reconfiguration); 102 | - `decimals`: number of digits to use after the decimal point. If 0, the Smart ASA is not divisible. If 1, the base unit of the Smart ASA is in tenth, it 2 it is in hundreds, if 3 it is in thousands, and so on; 103 | - `default_frozen`: True to freeze Smart ASA holdings by default; 104 | - `smart_asa_id`: asset ID of the *Underlying ASA*; 105 | - `frozen`: True to globally freeze Smart ASA transfers for all holders. 106 | 107 | Bytes Variables: 108 | 109 | - `unit_name`: name of a unit of the Smart ASA; 110 | - `name`: name of the Smart ASA; 111 | - `url`: URL with additional information on the Smart ASA; 112 | - `metadata_hash`: Smart ASA metadata hash; 113 | - `manager_addr`: Address of the account entitle to manage the configuration of the Smart ASA and destroy it; 114 | - `reserve_addr`: Address of the account entitled to mint and burn the Smart ASA; 115 | - `freeze_addr`: Address of the account entitled to freeze holdings or even globally freeze the Smart ASA; 116 | - `clawback_addr`: Address of the account that can clawback holdings of the Smart ASA. 117 | 118 | **The *Smart ASA App* of this reference implementation has been designed to control one ASA at a time. However the same app could be re-used for several Smart ASAs**. For this reason, the `smart_asa_id` variable has been added to the `GlobalState` to monitor the ID of the current *Underlying ASA* controlled by the application. This value is also stored into the local state of opted-in users, enforcing cross-checks between local and global states. This also avoids issues like unauthorized transfers (see [Security Considerations](https://github.com/algorandlabs/smart-asa#security-considerations) for more details). 119 | 120 | Bonus feature: This reference implementation also includes the Smart ASA global `frozen` variable. It can only be updated by the freeze address which has the authority of globally freezing the asset with a single action, rather than freezing accounts one by one. 121 | 122 | Finally, a new functional authority has been assigned to the `reserve` address of the Smart ASA. It is now the (_only_) entity in charge of `minting` and `burning` Smart ASAs (see the [Smart ASA Transfer](https://github.com/algorandlabs/smart-asa#smart-asa-transfer) interface for more details). 123 | 124 | #### Local State 125 | 126 | The opted-in users `LocalState` is defined as follows: 127 | 128 | Integer Variables: 129 | 130 | - `smart_asa_id`: asset ID of the *Underlying ASA* of the Smart ASA a user has opted-in; 131 | - `frozen`: True to freeze the holdings of the account. 132 | 133 | #### Self Validation 134 | 135 | The Smart ASA reference implementation enforces self validation of the `StateSchema`. On creation, it controls the size of the given schema for both the global and local states. The expected values are: 136 | 137 | | | Global | Local | 138 | |-----------|--------|-------| 139 | | **Ints** | 5 | 2 | 140 | | **Bytes** | 8 | 0 | 141 | 142 | #### Smart Contract ABI interface 143 | 144 | Smart Contract methods has been implemented to comply with the [Algorand ABI](https://developer.algorand.org/docs/get-details/dapps/smart-contracts/ABI/) interface. The whole ABI interface of the Smart ASA reference implementation can be found in `smart_asa_abi.json`. 145 | 146 | The validation checks on the ABI types are carried on the client side. The Smart Contract enforces the following on-chain checks: 147 | 148 | - `address` length must be equal to 32 bytes; 149 | - `Bool` values must be equal to 0 or 1. 150 | 151 | ## Smart ASA Methods 152 | 153 | Smart ASA reference implementation follows the ABI specified by ARC-20 to 154 | ensure full composability and interoperability with the rest of 155 | Algorand's ecosystem (e.g. wallets, chain explorers, external dApp, etc.). 156 | 157 | The implementation of the ABI relies on the new PyTeal ABI Router component, which 158 | automatically generates ABI JSON by using simple Python _decorators_ for Smart 159 | Contract methods. PyTeal ABI Router takes care of ABI types and methods' 160 | signatures encoding as well. 161 | 162 | ### Smart ASA App Create 163 | 164 | _Smart ASA Create_ is a `BareCall` (no argument needed) that instantiate the Smart 165 | ASA App, verifying the consistency of the `SateSchema` assigned to the create 166 | Application Call. This method initializes the whole Global State to default 167 | upon creation. 168 | 169 | ### Smart ASA App Opt-In 170 | 171 | _Smart ASA Opt-In_ represents the account opt-in to the Smart ASA. The argument `asset` represents the *Underlying ASA*. This method initializes the `LocalState` of the user. If the Smart ASA is `default_frozen` then the opting-in users are `frozen` too. 172 | 173 | ```json 174 | { 175 | "name": "asset_app_optin", 176 | "args": [ 177 | { 178 | "type": "asset", 179 | "name": "asset", 180 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 181 | }, 182 | { 183 | "type": "axfer", 184 | "name": "underlying_asa_optin", 185 | "desc": "Underlying ASA opt-in transaction." 186 | } 187 | ], 188 | "returns": { 189 | "type": "void" 190 | }, 191 | "desc": "Smart ASA atomic opt-in to Smart ASA App and Underlying ASA." 192 | } 193 | ``` 194 | 195 | ### Smart ASA App Close-Out 196 | 197 | _Smart ASA Close-Out_ closes out an account from the Smart ASA. It shadows the exact behavior of a traditional ASA. In particular, The argument `close_asset` represents the _Underlying ASA_ to be closed, whereas the `close_to` is the account to which all the remainder balance is sent on closing. This method removes the *Smart ASA App* `LocalState` from the calling account as well as closing out the _Underlying ASA_. The Smart ASA closing procedure proceeds as follows: 198 | 199 | 1. If the _Underlying ASA_ has been destroyed, then no checks is performed on `close_to` account; 200 | 2. If the user account is _frozen_ or the whole _Underlying ASA_ is _frozen_, then the `close_to` MUST be the Smart ASA Creator (*Smart ASA App*); 201 | 3. If the `close_to` account is not the Smart ASA Creator, then it MUST have opted-in to the Smart ASA. 202 | 203 | ```json 204 | { 205 | "name": "asset_app_closeout", 206 | "args": [ 207 | { 208 | "type": "asset", 209 | "name": "close_asset", 210 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 211 | }, 212 | { 213 | "type": "account", 214 | "name": "close_to", 215 | "desc": "Account to send all Smart ASA reminder to. If the asset/account is forzen then this must be set to Smart ASA Creator." 216 | } 217 | ], 218 | "returns": { 219 | "type": "void" 220 | }, 221 | "desc": "Smart ASA atomic close-out of Smart ASA App and Underlying ASA." 222 | } 223 | ``` 224 | 225 | ### Smart ASA Creation 226 | 227 | _Smart ASA Create_ is the creation method of a Smart ASA. It creates a new _Underlying ASA_ and parametrises the controlling Application with the given properties. 228 | 229 | ```json 230 | { 231 | "name": "asset_create", 232 | "args": [ 233 | { 234 | "type": "uint64", 235 | "name": "total", 236 | "desc": "The total number of base units of the Smart ASA to create." 237 | }, 238 | { 239 | "type": "uint32", 240 | "name": "decimals", 241 | "desc": "The number of digits to use after the decimal point when displaying the Smart ASA. If 0, the Smart ASA is not divisible." 242 | }, 243 | { 244 | "type": "bool", 245 | "name": "default_frozen", 246 | "desc": "Smart ASA default frozen status (True to freeze holdings by default)." 247 | }, 248 | { 249 | "type": "string", 250 | "name": "unit_name", 251 | "desc": "The name of a unit of Smart ASA." 252 | }, 253 | { 254 | "type": "string", 255 | "name": "name", 256 | "desc": "The name of the Smart ASA." 257 | }, 258 | { 259 | "type": "string", 260 | "name": "url", 261 | "desc": "Smart ASA external URL." 262 | }, 263 | { 264 | "type": "byte[]", 265 | "name": "metadata_hash", 266 | "desc": "Smart ASA metadata hash (suggested 32 bytes hash)." 267 | }, 268 | { 269 | "type": "address", 270 | "name": "manager_addr", 271 | "desc": "The address of the account that can manage the configuration of the Smart ASA and destroy it." 272 | }, 273 | { 274 | "type": "address", 275 | "name": "reserve_addr", 276 | "desc": "The address of the account that holds the reserve (non-minted) units of the asset and can mint or burn units of Smart ASA." 277 | }, 278 | { 279 | "type": "address", 280 | "name": "freeze_addr", 281 | "desc": "The address of the account that can freeze/unfreeze holdings of this Smart ASA globally or locally (specific accounts). If empty, freezing is not permitted." 282 | }, 283 | { 284 | "type": "address", 285 | "name": "clawback_addr", 286 | "desc": "The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted." 287 | } 288 | ], 289 | "returns": { 290 | "type": "uint64", 291 | "desc": "New Smart ASA ID." 292 | }, 293 | "desc": "Create a Smart ASA (triggers inner creation of an Underlying ASA)." 294 | } 295 | ``` 296 | 297 | ### Smart ASA Configuration 298 | 299 | _Smart ASA Configuration_ is the update method of a Smart ASA. It updates the 300 | parameters of an existing Smart ASA. Only the `manager` has the authority to 301 | reconfigure the asset by invoking this method. 302 | 303 | > The ABI method needs all Smart ASA fields, even those one not to be changed, by assigning the current value to the unchanged fields. The Smart ASA Client of this reference implementation abstracts this complexity taking care of unspecified fields by replicating current Smart ASA state for the unchanged fields as default. 304 | 305 | The following restrictions apply in this Smart ASA reference implementation: 306 | 307 | - `manager_addr`, `reserve_addr`, `freeze_addr` and `clawback_addr` addresses can no longer be configured once set to `ZERO_ADDRESS`; 308 | - `total` cannot be configured to a value lower than the current circulating supply. 309 | 310 | ```json 311 | { 312 | "name": "asset_config", 313 | "args": [ 314 | { 315 | "type": "asset", 316 | "name": "config_asset" 317 | }, 318 | { 319 | "type": "uint64", 320 | "name": "total", 321 | "desc": "The total number of base units of the Smart ASA to create. It can not be configured to less than its current circulating supply." 322 | }, 323 | { 324 | "type": "uint32", 325 | "name": "decimals", 326 | "desc": "The number of digits to use after the decimal point when displaying the Smart ASA. If 0, the Smart ASA is not divisible." 327 | }, 328 | { 329 | "type": "bool", 330 | "name": "default_frozen", 331 | "desc": "Smart ASA default frozen status (True to freeze holdings by default)." 332 | }, 333 | { 334 | "type": "string", 335 | "name": "unit_name", 336 | "desc": "The name of a unit of Smart ASA." 337 | }, 338 | { 339 | "type": "string", 340 | "name": "name", 341 | "desc": "The name of the Smart ASA." 342 | }, 343 | { 344 | "type": "string", 345 | "name": "url", 346 | "desc": "Smart ASA external URL." 347 | }, 348 | { 349 | "type": "byte[]", 350 | "name": "metadata_hash", 351 | "desc": "Smart ASA metadata hash (suggested 32 bytes hash)." 352 | }, 353 | { 354 | "type": "address", 355 | "name": "manager_addr", 356 | "desc": "The address of the account that can manage the configuration of the Smart ASA and destroy it." 357 | }, 358 | { 359 | "type": "address", 360 | "name": "reserve_addr", 361 | "desc": "The address of the account that holds the reserve (non-minted) units of the asset and can mint or burn units of Smart ASA." 362 | }, 363 | { 364 | "type": "address", 365 | "name": "freeze_addr", 366 | "desc": "The address of the account that can freeze/unfreeze holdings of this Smart ASA globally or locally (specific accounts). If empty, freezing is not permitted." 367 | }, 368 | { 369 | "type": "address", 370 | "name": "clawback_addr", 371 | "desc": "The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted." 372 | } 373 | ], 374 | "returns": { 375 | "type": "void" 376 | }, 377 | "desc": "Configure the Smart ASA. Use existing values for unchanged parameters. Setting Smart ASA roles to zero-address is irreversible." 378 | } 379 | ``` 380 | 381 | ### Smart ASA Transfer 382 | 383 | _Smart ASA Transfer_ is the asset transfer method of a Smart ASA. It defines the transfer of an asset between an `asset_sender` and `asset_receiver` specifying the `asset_amount` to be transferred. This method automatically distinguishes four types of transfer, such as `mint`, `burn`, `clawback`, and regular `transfer`. 384 | 385 | ```json 386 | { 387 | "name": "asset_transfer", 388 | "args": [ 389 | { 390 | "type": "asset", 391 | "name": "xfer_asset", 392 | "desc": "Smart ASA ID to transfer." 393 | }, 394 | { 395 | "type": "uint64", 396 | "name": "asset_amount", 397 | "desc": "Smart ASA amount to transfer." 398 | }, 399 | { 400 | "type": "account", 401 | "name": "asset_sender", 402 | "desc": "Smart ASA sender, for regular transfer this must be equal to the Smart ASA App caller." 403 | }, 404 | { 405 | "type": "account", 406 | "name": "asset_receiver", 407 | "desc": "The recipient of the Smart ASA transfer." 408 | } 409 | ], 410 | "returns": { 411 | "type": "void" 412 | }, 413 | "desc": "Smart ASA transfers: regular, clawback (Clawback Address), mint or burn (Reserve Address)." 414 | } 415 | ``` 416 | 417 | #### Mint 418 | 419 | In the reference implementation only the `reserve` address can _mint_ a Smart ASA. A minting succeeds if the following conditions are met: 420 | 421 | - the Smart ASA is not globally `frozen`; 422 | - `asset_sender` is the *Smart ASA App*; 423 | - `asset_receiver` is not `frozen`; 424 | - `asset_receiver` Smart ASA ID in Local State is up-to-date; 425 | - `asset_amount` does not exceed the outstanding available supply of the Smart ASA. 426 | 427 | > Reference implementation checks that `smart_asa_id` is _up-to-date_ in Local 428 | > State since the Smart ASA App could create a new Underlying ASA (if the 429 | > previous one has been dystroied by the Manager Address). This requires users 430 | > to opt-in again and initialize accordingly a coherent `frozen` status for the 431 | > new Smart ASA (which could potentially have been created as `default_frozen`). 432 | 433 | #### Burn 434 | 435 | In the reference implementation only the `reserve` address can burn a Smart ASA. A burning succeeds if the following conditions are met: 436 | 437 | - the Smart ASA is not globally `frozen`; 438 | - `asset_sender` is the `reserve` address; 439 | - `asset_sender` is not `frozen`; 440 | - `asset_sender` Smart ASA ID in Local State is up-to-date; 441 | - `asset_receiver` is the *Smart ASA App*. 442 | 443 | #### Clawback 444 | 445 | The `clawback` address of a Smart ASA can invoke a clawback transfer from and to any asset holder (or revoke an asset). A clawback succeeds if the following conditions are met: 446 | 447 | - `asset_sender` Smart ASA ID in Local State is up-to-date; 448 | - `asset_receiver` Smart ASA ID in Local State is up-to-date;. 449 | 450 | Checking that Smart ASA ID in Local State is up-to-date both for `asset_sender` and `asset_receiver` implicitly verifies that both users are opted-in to the *Smart ASA App*. This ensures that _minting_ and _burning_ can not be executed as _clawback_, since the *Smart ASA App* can not opt-in to itself. 451 | 452 | #### Transfer 453 | 454 | A regular transfer of a Smart ASA can be invoked by any opted-in asset holder. It succeeds if the following conditions are met: 455 | 456 | - the Smart ASA is not globally `frozen`; 457 | - `asset_sender` is not `frozen`; 458 | - `asset_sender` Smart ASA ID in Local State is up-to-date; 459 | - `asset_receiver` is not `frozen`; 460 | - `asset_receiver` Smart ASA ID in Local State is up-to-date. 461 | 462 | ### Smart ASA Global Freeze 463 | 464 | _Smart ASA Global Freeze_ is the freeze method of a Smart ASA. It enables the `freeze` address to globally freeze a Smart ASA. A frozen Smart ASA cannot be transferred, minted or burned. 465 | 466 | ```json 467 | { 468 | "name": "asset_freeze", 469 | "args": [ 470 | { 471 | "type": "asset", 472 | "name": "freeze_asset", 473 | "desc": "Smart ASA ID to freeze/unfreeze." 474 | }, 475 | { 476 | "type": "bool", 477 | "name": "asset_frozen", 478 | "desc": "Smart ASA ID forzen status." 479 | } 480 | ], 481 | "returns": { 482 | "type": "void" 483 | }, 484 | "desc": "Smart ASA global freeze (all accounts), called by the Freeze Address." 485 | } 486 | ``` 487 | 488 | ### Smart ASA Account Freeze 489 | 490 | _Smart ASA Account Freeze_ is the account freeze method of a Smart ASA. It enables the `freeze` address to freeze a Smart ASA holder. Freezed accounts cannot receive nor send the asset. 491 | 492 | ```json 493 | { 494 | "name": "account_freeze", 495 | "args": [ 496 | { 497 | "type": "asset", 498 | "name": "freeze_asset", 499 | "desc": "Smart ASA ID to freeze/unfreeze." 500 | }, 501 | { 502 | "type": "account", 503 | "name": "freeze_account", 504 | "desc": "Account to freeze/unfreeze." 505 | }, 506 | { 507 | "type": "bool", 508 | "name": "asset_frozen", 509 | "desc": "Smart ASA ID forzen status." 510 | } 511 | ], 512 | "returns": { 513 | "type": "void" 514 | }, 515 | "desc": "Smart ASA local freeze (account specific), called by the Freeze Address." 516 | } 517 | ``` 518 | 519 | ### Smart ASA Destroy 520 | 521 | _Smart ASA Destroy_ is the destroy method of a Smart ASA. In this reference implementation only the `manager` can invoke the Smart ASA destroy. This method clears the `GlobalState` schema of a Smart ASA, destroying any previous configuration. 522 | 523 | > A Smart ASA can be destroyed if and only if the `circulating supply = 0`. After a Smart ASA destroy, users remain opted-in to the *Smart ASA App*, but with an outdated `smart_asa_id` in their local state. The section [Security Considerations](https://github.com/algorandlabs/smart-asa#security-considerations) discusses the side effects of a Smart ASA destroy. 524 | 525 | ```json 526 | { 527 | "name": "asset_destroy", 528 | "args": [ 529 | { 530 | "type": "asset", 531 | "name": "destroy_asset", 532 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 533 | } 534 | ], 535 | "returns": { 536 | "type": "void" 537 | }, 538 | "desc": "Destroy the Underlying ASA, must be called by Manager Address." 539 | } 540 | ``` 541 | 542 | ### Smart ASA Getters 543 | 544 | _Getter_ methods expose relevant information of a Smart ASA. To retrieve the whole configuration you can query the ABI method `get_asset_config` which returns a Tuple with all the configuration parameters! 545 | 546 | The reference implementation also exposes the following getters: 547 | 548 | - `get_asset_is_frozen`: which returns `True` if the Smart ASA is globally frozen; 549 | - `get_account_is_frozen`: which returns `True` if a given account is frozen; 550 | - `get_circulating_supply`: which returns the current circulating supply of a smart ASA; 551 | - `get_optin_min_balance`: which returns the minimum balance (in ALGO) required to opt-in the Smart ASA. 552 | 553 | Getters ABI interface example: 554 | 555 | 556 | ```json 557 | { 558 | "name": "get_", 559 | "args": [ 560 | {"name": "asset", "type": "asset"} 561 | ], 562 | "returns": {"type": ""} 563 | } 564 | ``` 565 | 566 | ## Smart ASA CLI 567 | The Smart ASA CLI has been conceived to offer the community a comprehensive and intuitive tool to interact with all the functionalities of the Smart ASA of this reference implementation. The CLI, as-is, is intended for testing purposes and can only be used within an Algorand Sandbox environment. 568 | 569 | ### Install 570 | 571 | **CLI Requirement: Algorand Sandbox** (try it with `dev` mode first!) 572 | 573 | The `Pipfile` contains all the dependencies to install the Smart ASA CLI using 574 | `pipenv` entering: 575 | 576 | ```shell 577 | pipenv install 578 | ``` 579 | 580 | ### Usage 581 | The Smart ASA CLI plays the same role as `goal asset` to facilitate a seamless 582 | understanding of this new "smarter" ASA. 583 | 584 | The CLI has been built with `docopt`, which provides an intuitive and standard 585 | command line usage: 586 | 587 | - `<...>` identify mandatory positional arguments; 588 | - `[...]` identify optional arguments; 589 | - `(...|...)` identify mandatory mutually exclusive arguments; 590 | - `[...|...]` identify optional mutually exclusive arguments; 591 | - `--arguments` could be followed by a `` (if required) or not; 592 | 593 | All the ``s (e.g. ``, ``, etc.) must be addresses of 594 | a wallet account managed by `sandbox`'s KMD. 595 | 596 | Using the command line you can perform all the actions over a Smart ASA, just 597 | like an ASA! 598 | 599 | ```shell 600 | Smart ASA (ARC-20 reference implementation) 601 | 602 | Usage: 603 | smart_asa create [--decimals=] [--default-frozen=] 604 | [--name=] [--unit-name=] [--metadata-hash=] 605 | [--url=] [--manager=] [--reserve=] 606 | [--freeze=] [--clawback=] 607 | smart_asa config [--new-total=] [--new-decimals=] 608 | [--new-default-frozen=] [--new-name=] 609 | [--new-unit-name=] [--new-metadata-hash=] 610 | [--new-url=] [--new-manager=] [--new-reserve=] 611 | [--new-freeze=] [--new-clawback=] 612 | smart_asa destroy 613 | smart_asa freeze (--asset | --account=) 614 | smart_asa optin 615 | smart_asa optout 616 | smart_asa send 617 | [--reserve= | --clawback=] 618 | smart_asa info [--account=] 619 | smart_asa get [--account=] 620 | smart_asa [--help] 621 | 622 | Commands: 623 | create Create a Smart ASA 624 | config Configure a Smart ASA 625 | destroy Destroy a Smart ASA 626 | freeze Freeze whole Smart ASA or specific account, = 1 is forzen 627 | optin Optin Smart ASAs 628 | optout Optout Smart ASAs 629 | send Transfer Smart ASAs 630 | info Look up current parameters for Smart ASA or specific account 631 | get Look up a parameter for Smart ASA 632 | 633 | Options: 634 | -h, --help 635 | -d, --decimals= [default: 0] 636 | -z, --default-frozen= [default: 0] 637 | -n, --name= [default: ] 638 | -u, --unit-name= [default: ] 639 | -l, --url= [default: ] 640 | -s, --metadata-hash= [default: ] 641 | -m, --manager= Default to Smart ASA Creator 642 | -r, --reserve= Default to Smart ASA Creator 643 | -f, --freeze= Default to Smart ASA Creator 644 | -c, --clawback= Default to Smart ASA Creator 645 | ``` 646 | 647 | ### Create Smart ASA NFT 648 | Let's create a beautiful 🔴 Smart ASA NFT (non-fractional for the moment)... 649 | 650 | ```shell 651 | python3 smart_asa_cli.py create KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 1 --name Red --unit-name 🔴 652 | 653 | --- Creating Smart ASA App... 654 | --- Smart ASA App ID: 2988 655 | 656 | --- Funding Smart ASA App with 1 ALGO... 657 | 658 | --- Creating Smart ASA... 659 | --- Created Smart ASA with ID: 2991 660 | ``` 661 | 662 | The Smart ASA is created directly by the Smart ASA App, so upon creation the 663 | whole supply is stored in Smart ASA App account. A *minting* action is required 664 | to put units of Smart ASA in circulation (see 665 | [Mint Smart ASA NFT](./README.md#mint-smart-asa-nft)). 666 | 667 | ```shell 668 | python3 smart_asa_cli.py info 2991 669 | 670 | Asset ID: 2991 671 | App ID: 2988 672 | App Address: T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 673 | Creator: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 674 | Asset name: Red 675 | 676 | Unit name: 🔴 677 | 678 | Maximum issue: 1 🔴 679 | Issued: 0 🔴 680 | Decimals: 0 681 | Global frozen: False 682 | Default frozen: False 683 | Manager address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 684 | Reserve address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 685 | Freeze address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 686 | Clawback address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 687 | ``` 688 | 689 | ### Fractionalize Smart ASA NFT 690 | One of the amazing new feature of Smart ASAs is that they are **completely** 691 | re-configurable after creation! Exactly: you can even reconfigure their 692 | `total` or their `decimals`! 693 | 694 | So let's use this new cool feature to **fractionalize** the Smart ASA NFT after 695 | its creation by setting the new `` to 100 and `` to 2! 696 | 697 | ```shell 698 | python3 smart_asa_cli.py config 2991 KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ --new-total 100 --new-decimals 2 699 | 700 | --- Configuring Smart ASA 2991... 701 | --- Smart ASA 2991 configured! 702 | ``` 703 | 704 | ```shell 705 | python3 smart_asa_cli.py info 2991 706 | 707 | Asset ID: 2991 708 | App ID: 2988 709 | App Address: T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 710 | Creator: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 711 | Asset name: Red 712 | 713 | Unit name: 🔴 714 | 715 | Maximum issue: 100 🔴 <-- 😱 716 | Issued: 0 🔴 717 | Decimals: 2 <-- 😱 718 | Global frozen: False 719 | Default frozen: False 720 | Manager address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 721 | Reserve address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 722 | Freeze address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 723 | Clawback address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 724 | ``` 725 | 726 | ### Smart ASA NFT opt-in 727 | We can now opt-in the Smart ASA using the `optin` command that manages both the 728 | undelying ASA opt-in and the Smart ASA App opt-in under the hood. 729 | 730 | > Note that opt-in to Smart ASA App is required only if the Smart ASA need 731 | > local state (e.g. *account frozen*). 732 | 733 | ```shell 734 | python3 smart_asa_cli.py optin 2991 KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 735 | 736 | --- Opt-in Smart ASA 2991... 737 | 738 | --- Smart ASA 2991 state: 739 | {'frozen': 0, 'smart_asa_id': 2991} 740 | ``` 741 | 742 | ### Mint Smart ASA NFT 743 | 744 | Only Smart ASA Reserve Address can mint units of Smart ASA from the Smart ASA 745 | App, with the following restrictions: 746 | 747 | - Smart ASA can not be *over minted* (putting in circulation more units than 748 | `total`); 749 | - Smart ASA can not be minted if the *asset is global frozen*; 750 | - Smart ASA can not be minted if the minting receiver *account is frozen*; 751 | 752 | ```shell 753 | python3 smart_asa_cli.py send 2991 T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 754 | KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 100 755 | --reserve KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 756 | 757 | --- Minting 100 units of Smart ASA 2991 758 | from T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 759 | to KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ... 760 | --- Confirmed! 761 | ``` 762 | 763 | ```shell 764 | python3 smart_asa_cli.py info 2991 765 | 766 | Asset ID: 2991 767 | App ID: 2988 768 | App Address: T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 769 | Creator: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 770 | Asset name: Red 771 | 772 | Unit name: 🔴 773 | 774 | Maximum issue: 100 🔴 775 | Issued: 100 🔴 <-- 👀 776 | Decimals: 2 777 | Global frozen: False 778 | Default frozen: False 779 | Manager address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 780 | Reserve address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 781 | Freeze address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 782 | Clawback address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 783 | ``` 784 | 785 | ### Smart NFT ASA global freeze 786 | Differently from regular ASA, Smart ASA can now be *globally frozen* by Freeze 787 | Account, meaning that the whole Smart ASA in atomically frozen regardless the 788 | particular *frozen state* of each account (which continues to be managed in 789 | the same way as regular ASA). 790 | 791 | Let's freeze the whole Smart ASA before starting administrative operations on 792 | it: 793 | 794 | ```shell 795 | python3 smart_asa_cli.py freeze 2991 KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ --asset 1 796 | 797 | --- Freezing Smart ASA 2991... 798 | ``` 799 | 800 | ```shell 801 | python3 smart_asa_cli.py info 2991 802 | 803 | Asset ID: 2991 804 | App ID: 2988 805 | App Address: T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 806 | Creator: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 807 | Asset name: Red 808 | 809 | Unit name: 🔴 810 | 811 | Maximum issue: 100 🔴 812 | Issued: 100 🔴 813 | Decimals: 2 814 | Global frozen: True <-- 😱 815 | Default frozen: False 816 | Manager address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 817 | Reserve address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 818 | Freeze address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 819 | Clawback address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 820 | ``` 821 | 822 | ### Smart NFT ASA rename 823 | Now that the whole Smart ASA is globally frozen, let's take advantage again of 824 | Smart ASA full reconfigurability to change its `--name` and `--unit-name`! 825 | 826 | ```shell 827 | python3 smart_asa_cli.py config 2991 KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ --new-name Blue --new-unit-name 🔵 828 | 829 | --- Configuring Smart ASA 2991... 830 | --- Smart ASA 2991 configured! 831 | ``` 832 | 833 | ```shell 834 | python3 smart_asa_cli.py info 2991 835 | 836 | Asset ID: 2991 837 | App ID: 2988 838 | App Address: T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 839 | Creator: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 840 | Asset name: Blue <-- 😱 841 | 842 | Unit name: 🔵 <-- 😱 843 | 844 | Maximum issue: 100 🔵 845 | Issued: 100 🔵 846 | Decimals: 2 847 | Global frozen: True 848 | Default frozen: False 849 | Manager address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 850 | Reserve address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 851 | Freeze address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 852 | Clawback address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 853 | ``` 854 | 855 | ### Smart NFT ASA global unfreeze 856 | The Smart ASA is all set! Let's *unfreeze* it globally! 857 | 858 | ```shell 859 | python3 smart_asa_cli.py freeze 2991 KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ --asset 0 860 | 861 | --- Unfreezing Smart ASA 2991... 862 | ``` 863 | 864 | ```shell 865 | python3 smart_asa_cli.py info 2991 866 | 867 | Asset ID: 2991 868 | App ID: 2988 869 | App Address: T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 870 | Creator: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 871 | Asset name: Blue 872 | 873 | Unit name: 🔵 874 | 875 | Maximum issue: 100 🔵 876 | Issued: 100 🔵 877 | Decimals: 2 878 | Global frozen: False <-- 😱 879 | Default frozen: False 880 | Manager address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 881 | Reserve address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 882 | Freeze address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 883 | Clawback address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 884 | ``` 885 | 886 | ### Smart NFT ASA burn 887 | Another exclusive capability of Smart ASA Reserve Address is *burning* the 888 | Smart ASA with the following limitation: 889 | 890 | - Smart ASA can not be burned if the *asset is global frozen*; 891 | - Smart ASA can not be burned if the Reserve *account is frozen*; 892 | 893 | ```shell 894 | python3 smart_asa_cli.py send 2991 KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 895 | T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 100 896 | --reserve KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 897 | 898 | --- Burning 100 units of Smart ASA 2991 899 | from KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 900 | to T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU... 901 | --- Confirmed! 902 | ``` 903 | 904 | ```shell 905 | python3 smart_asa_cli.py info 2991 906 | 907 | Asset ID: 2991 908 | App ID: 2988 909 | App Address: T6QBA5AXSJMBG55Y2BVDR6MN5KTXHHLU7LWDY3LGZNAPGIKDOWMP4GF5PU 910 | Creator: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 911 | Asset name: Blue 912 | 913 | Unit name: 🔵 914 | 915 | Maximum issue: 100 🔵 916 | Issued: 0 🔵 <-- 👀 917 | Decimals: 2 918 | Global frozen: False 919 | Default frozen: False 920 | Manager address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 921 | Reserve address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 922 | Freeze address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 923 | Clawback address: KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 924 | ``` 925 | 926 | ### Smart ASA destroy 927 | Similarly to regular ASA, Smart ASA can be destroyed by Smart ASA Manager 928 | Address if and only if the Smart ASA Creator hold the `total` supply. 929 | 930 | ```shell 931 | python3 smart_asa_cli.py destroy 2991 KAVHOSWPO3XLBL5Q7FFOTPHAIRAT6DRDXUYGSLQOAEOPRSAXJKKPMHWLLQ 932 | 933 | --- Destroying Smart ASA 2991... 934 | --- Smart ASA 2991 destroyed! 935 | ``` 936 | 937 | ## Security Considerations 938 | 939 | ### Prevent malicious Clear State 940 | 941 | A malicious user could attempt to Clear its Local State to hack the `frozen` state of a Smart ASA. Consider the following scenario: 942 | 943 | - Smart ASA `default_frozen = False`; 944 | - Eve is regularly opted-in to the Smart ASA; 945 | - Eve receives 5 Smart ASAs from Bob (Smart ASA manager and freezer) and get freezed afterwards; 946 | - Eve can now Clear its Local State and Opt-In again to reset its `frozen` state and be free to spend the Smart ASAs. 947 | 948 | To avoid this situation, the reference implementation introduces: 949 | - *Opt-In condition*: set `frozen` status of the account to `True` if 950 | upon the opt-in, after a Clear State, the account holds an amount of Smart ASA. 951 | 952 | ### Conscious Smart ASA Destroy 953 | 954 | Upon an `asset_destroy`, the `GlobalState` of the *Smart ASA App* is re-initialized and the _Underlying ASA_ destroyed. However, the destroy does not affect users' `LocalState`. Let's consider the case a `manager` invokes an `asset_destroy` over Smart ASA `A` and afterwards an `asset_create` to instantiate Smart ASA `B` with the same *Smart ASA App*. 955 | 956 | - Eve was opted-in to *Smart ASA App* and was not frozen; 957 | - Bob (manager) destroys Smart ASA `A` (assuming `circulating_supply = 0`); 958 | - Bob creates Smart ASA `B` with param `default_frozen = True`; 959 | - Eve is opted-in with `frozen = False`; 960 | - Eve can freely receive and spend Smart ASA `B`. 961 | 962 | To avoid this issue, the reference implementation includes the current `smart_asa_id` in both the `GlobalState` and `LocalState`. Smart ASA transfers can now be approved only for users opted-in with the current _Underlying ASA_. 963 | 964 | ## Conclusions 965 | 966 | Smart ASA reference implementation is a building block that shows how regular ASA can be turned into a more powerful and sophisticated L1 tool. By adopting ABI the Smart ASA will be easily interoperable and composable with the rest of Algorand's ecosystem (e.g. wallets, chain explorers, external dApp, etc.). 967 | 968 | This reference implementation is intended to be used as initial step for more specific and customized transferability logic like: royalties, DAOs' assets, NFTs, in-game assets etc. 969 | 970 | We encourage the community to expand and customize this new tool to fit 971 | specific dApp! 972 | 973 | Enjoy experimenting and building with Smart ASA! 974 | 975 | ## Credits to community 976 | 977 | Thanks to everyone who contributed or starred the repository! ⭐ 978 | 979 | [![Stargazers repo roster for @algorandlabs/smart-asa](https://reporoster.com/stars/dark/algorandlabs/smart-asa)](https://github.com/algorandlabs/smart-asa/stargazers) 980 | -------------------------------------------------------------------------------- /account.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import base64 3 | from typing import Any, Optional, Union, cast 4 | 5 | import algosdk 6 | from algosdk import encoding 7 | from algosdk.future import transaction 8 | from algosdk.v2client import algod 9 | from algosdk.atomic_transaction_composer import ( 10 | ABIResult, 11 | AtomicTransactionComposer, 12 | TransactionSigner, 13 | TransactionWithSigner, 14 | ) 15 | 16 | from utils import ( 17 | assemble_program, 18 | get_global_state, 19 | get_local_state, 20 | get_params, 21 | ) 22 | 23 | 24 | @dataclasses.dataclass(frozen=True) 25 | class Account(TransactionSigner): 26 | address: str 27 | private_key: Optional[str] = None 28 | algod_client: Optional[algod.AlgodClient] = None 29 | 30 | @classmethod 31 | def create(cls, **kwargs) -> "Account": 32 | private_key, address = algosdk.account.generate_account() 33 | return cls(cast(str, address), private_key, **kwargs) 34 | 35 | @property 36 | def decoded_address(self): 37 | return encoding.decode_address(self.address) 38 | 39 | def sign(self, txn): 40 | assert self.private_key 41 | return txn.sign(self.private_key) 42 | 43 | def sign_transactions( 44 | self, txn_group: list[transaction.Transaction], indexes: list[int] 45 | ) -> list: 46 | # Enables using `self` with `AtomicTransactionComposer` 47 | stxns = [] 48 | for i in indexes: 49 | stxn = self.sign(txn_group[i]) # type: ignore 50 | stxns.append(stxn) 51 | return stxns 52 | 53 | def sign_send_wait( 54 | self, 55 | txn: transaction.Transaction, 56 | save_txn: str = None, 57 | ): 58 | """Sign a transaction, submit it, and wait for its confirmation.""" 59 | assert self.algod_client 60 | 61 | signed_txn = self.sign(txn) 62 | tx_id = signed_txn.transaction.get_txid() 63 | if save_txn: 64 | transaction.write_to_file([signed_txn], save_txn, overwrite=True) 65 | 66 | try: 67 | self.algod_client.send_transactions([signed_txn]) 68 | 69 | transaction.wait_for_confirmation(self.algod_client, tx_id) 70 | 71 | return self.algod_client.pending_transaction_info(tx_id) 72 | 73 | except algosdk.error.AlgodHTTPError as err: 74 | drr = transaction.create_dryrun(self.algod_client, [signed_txn]) 75 | filename = "dryrun.msgp" 76 | with open(filename, "wb") as f: 77 | f.write(base64.b64decode(encoding.msgpack_encode(drr))) 78 | raise err 79 | 80 | def _get_params(self, *args, **kwargs) -> transaction.SuggestedParams: 81 | assert self.algod_client 82 | return get_params(self.algod_client, *args, **kwargs) 83 | 84 | def pay(self, receiver: Union["Account", "AppAccount"], amount: int): 85 | txn = transaction.PaymentTxn( 86 | self.address, self._get_params(), receiver.address, amount 87 | ) 88 | return self.sign_send_wait(txn) 89 | 90 | def abi_call( 91 | self, 92 | method, 93 | *args, 94 | app: Union[int, "AppAccount"], 95 | group_extra_txns: Optional[list[TransactionWithSigner]] = None, 96 | on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, 97 | fee: Optional[int] = None, 98 | max_wait_rounds: int = 10, 99 | save_abi_call: Optional[str] = None, 100 | ) -> ABIResult: 101 | """ 102 | ABI call from `sender` to `app` `method`, with `*args`. Txn-type args are supplied 103 | as normal arguments. 104 | Use `group_extra_txns` to append other (non argument) transactions to the ABI call in an 105 | atomic group. 106 | """ 107 | assert self.algod_client 108 | 109 | if isinstance(app, AppAccount): 110 | app = app.app_id 111 | 112 | encoded_args = [] 113 | for arg in args: 114 | if isinstance(arg, Account): 115 | encoded_args.append(arg.address) 116 | else: 117 | encoded_args.append(arg) 118 | 119 | atc = AtomicTransactionComposer() 120 | atc.add_method_call( 121 | app_id=app, 122 | method=method, 123 | method_args=encoded_args, 124 | sp=self._get_params(fee), 125 | sender=self.address, 126 | signer=self, 127 | on_complete=on_complete, 128 | ) 129 | 130 | if group_extra_txns is not None: 131 | for transaction_with_signer in group_extra_txns: 132 | atc.add_transaction(transaction_with_signer) 133 | 134 | atc.build_group() 135 | atc.gather_signatures() 136 | if save_abi_call: 137 | transaction.write_to_file(atc.signed_txns, save_abi_call, overwrite=True) 138 | try: 139 | atc_result = atc.execute(self.algod_client, max_wait_rounds) 140 | logged_result = atc_result.abi_results[0] # type: ignore 141 | if logged_result.decode_error: 142 | print("ABI decode error:", logged_result.decode_error) 143 | print("\tRaw value:", logged_result.raw_value) 144 | if not logged_result or logged_result.return_value is None: 145 | return None 146 | return logged_result.return_value 147 | 148 | except algosdk.error.AlgodHTTPError as err: 149 | drr = transaction.create_dryrun(self.algod_client, atc.signed_txns) 150 | filename = "dryrun.msgp" 151 | with open(filename, "wb") as f: 152 | f.write(base64.b64decode(encoding.msgpack_encode(drr))) 153 | raise err 154 | 155 | def create_asset(self, **kwargs) -> int: 156 | """Create an asset and return its ID.""" 157 | args = { 158 | "sp": self._get_params(), 159 | "total": 1, 160 | "default_frozen": False, 161 | "unit_name": "TMP", 162 | "asset_name": "TMP", 163 | "manager": self.address, 164 | "reserve": self.address, 165 | "freeze": self.address, 166 | "clawback": self.address, 167 | "decimals": 0, 168 | } 169 | args.update(**kwargs) 170 | txn = transaction.AssetConfigTxn(sender=self.address, **args) 171 | 172 | ptx = self.sign_send_wait(txn) 173 | return ptx["asset-index"] 174 | 175 | def optin_to_asset(self, asset_id: int) -> dict: 176 | txn = transaction.AssetTransferTxn( 177 | sender=self.address, 178 | sp=self._get_params(), 179 | receiver=self.address, 180 | amt=0, 181 | index=asset_id, 182 | ) 183 | return self.sign_send_wait(txn) 184 | 185 | def close_asset_to( 186 | self, 187 | asset_id: int, 188 | close_to_account: "Account", 189 | amount: int = 0, 190 | receiver: Optional["Account"] = None, 191 | ) -> dict: 192 | txn = transaction.AssetTransferTxn( 193 | sender=self.address, 194 | sp=self._get_params(), 195 | receiver=self.address if not receiver else receiver.address, 196 | amt=amount, 197 | index=asset_id, 198 | close_assets_to=close_to_account.address, 199 | ) 200 | return self.sign_send_wait(txn) 201 | 202 | def optin_to_application( 203 | self, 204 | asc_id: int, 205 | app_args: Any = None, 206 | accounts: Any = None, 207 | foreign_apps: Any = None, 208 | foreign_assets: Any = None, 209 | ) -> dict: 210 | txn = transaction.ApplicationOptInTxn( 211 | sender=self.address, 212 | sp=self._get_params(), 213 | index=asc_id, 214 | app_args=app_args, 215 | accounts=accounts, 216 | foreign_apps=foreign_apps, 217 | foreign_assets=foreign_assets, 218 | ) 219 | return self.sign_send_wait(txn) 220 | 221 | def transfer_asset(self, receiver: "Account", asset_id: int, amount: int) -> dict: 222 | txn = transaction.AssetTransferTxn( 223 | sender=self.address, 224 | sp=self._get_params(), 225 | receiver=receiver.address, 226 | amt=amount, 227 | index=asset_id, 228 | ) 229 | return self.sign_send_wait(txn) 230 | 231 | def close_out_application(self, app_id: int) -> dict: 232 | txn = transaction.ApplicationCloseOutTxn( 233 | self.address, self._get_params(), app_id 234 | ) 235 | return self.sign_send_wait(txn) 236 | 237 | def balance(self) -> dict[int, int]: 238 | """Returns a dict mappgin each asset id to the balance amount; the algo balance has key 0.""" 239 | assert self.algod_client 240 | account_info = self.algod_client.account_info(self.address) 241 | balances = {a["asset-id"]: int(a["amount"]) for a in account_info["assets"]} 242 | balances[0] = int(account_info["amount"]) 243 | return balances 244 | 245 | def asa_balance(self, asa_idx: int) -> int: 246 | return self.balance().get(asa_idx, 0) 247 | 248 | def app_local_state( 249 | self, app: Union["AppAccount", int] 250 | ) -> dict[str, Union[bytes, int]]: 251 | assert self.algod_client 252 | if isinstance(app, AppAccount): 253 | app = app.app_id 254 | return get_local_state(self.algod_client, self.address, app) 255 | 256 | def local_state(self) -> list[dict]: 257 | assert self.algod_client 258 | return self.algod_client.account_info(self.address)["apps-local-state"] 259 | 260 | def create_asc( 261 | self, 262 | approval_program: str, 263 | clear_program: str, 264 | global_schema=transaction.StateSchema(0, 0), 265 | local_schema=transaction.StateSchema(0, 0), 266 | on_complete=transaction.OnComplete.NoOpOC, 267 | ) -> "AppAccount": 268 | approval_program = assemble_program(self.algod_client, approval_program) 269 | clear_program = assemble_program(self.algod_client, clear_program) 270 | 271 | txn = transaction.ApplicationCreateTxn( 272 | self.address, 273 | self._get_params(), 274 | on_complete, 275 | approval_program, 276 | clear_program, 277 | global_schema, 278 | local_schema, 279 | extra_pages=(len(approval_program) + len(clear_program)) // 2048, 280 | ) 281 | 282 | transaction_response = self.sign_send_wait(txn) 283 | return AppAccount.from_app_id( 284 | transaction_response["application-index"], algod_client=self.algod_client 285 | ) 286 | 287 | def update_application( 288 | self, 289 | approval_program: str, 290 | clear_program: str, 291 | app_id: int, 292 | app_args: Optional[list] = None, 293 | accounts: Optional[list[str]] = None, 294 | foreign_apps: Optional[list[int]] = None, 295 | foreign_assets: Optional[list[int]] = None, 296 | ) -> dict: 297 | 298 | approval_program = assemble_program(self.algod_client, approval_program) 299 | clear_program = assemble_program(self.algod_client, clear_program) 300 | 301 | txn = transaction.ApplicationUpdateTxn( 302 | sender=self.address, 303 | sp=self._get_params(), 304 | index=app_id, 305 | approval_program=approval_program, 306 | clear_program=clear_program, 307 | app_args=app_args, 308 | accounts=accounts, 309 | foreign_apps=foreign_apps, 310 | foreign_assets=foreign_assets, 311 | ) 312 | 313 | return self.sign_send_wait(txn) 314 | 315 | def delete_application( 316 | self, 317 | app_id: int, 318 | app_args: Optional[list] = None, 319 | accounts: Optional[list[str]] = None, 320 | foreign_apps: Optional[list[int]] = None, 321 | foreign_assets: Optional[list[int]] = None, 322 | ) -> dict: 323 | 324 | txn = transaction.ApplicationDeleteTxn( 325 | sender=self.address, 326 | sp=self._get_params(), 327 | index=app_id, 328 | app_args=app_args, 329 | accounts=accounts, 330 | foreign_apps=foreign_apps, 331 | foreign_assets=foreign_assets, 332 | ) 333 | 334 | return self.sign_send_wait(txn) 335 | 336 | def clear_state( 337 | self, 338 | app_id: int, 339 | app_args: Optional[list] = None, 340 | accounts: Optional[list[str]] = None, 341 | foreign_apps: Optional[list[int]] = None, 342 | foreign_assets: Optional[list[int]] = None, 343 | ) -> dict: 344 | txn = transaction.ApplicationClearStateTxn( 345 | sender=self.address, 346 | sp=self._get_params(), 347 | index=app_id, 348 | app_args=app_args, 349 | accounts=accounts, 350 | foreign_apps=foreign_apps, 351 | foreign_assets=foreign_assets, 352 | ) 353 | return self.sign_send_wait(txn) 354 | 355 | 356 | @dataclasses.dataclass(frozen=True) 357 | class AppAccount(Account): 358 | app_id: int = None 359 | 360 | @classmethod 361 | def from_app_id(cls, app_id: int, **kwargs) -> "AppAccount": 362 | return cls( 363 | app_id=app_id, 364 | address=cast( 365 | str, 366 | encoding.encode_address( 367 | encoding.checksum(b"appID" + app_id.to_bytes(8, "big")) 368 | ), 369 | ), 370 | **kwargs, 371 | ) 372 | 373 | def global_state(self) -> dict[str, Union[bytes, int]]: 374 | assert self.algod_client 375 | return get_global_state(self.algod_client, self.app_id) 376 | 377 | def app_local_state( 378 | self, account: Union[Account, str] 379 | ) -> dict[str, Union[bytes, int]]: 380 | assert self.algod_client 381 | if isinstance(account, Account): 382 | account = account.address 383 | return get_local_state(self.algod_client, account, self.app_id) 384 | -------------------------------------------------------------------------------- /sandbox.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import time 3 | from algosdk.kmd import KMDClient 4 | from algosdk.v2client import algod 5 | from algosdk.wallet import Wallet 6 | 7 | from account import Account 8 | from utils import get_last_round, get_last_timestamp 9 | 10 | 11 | class Sandbox: 12 | ALGOD_ADDRESS = "http://localhost:4001" 13 | ALGOD_TOKEN = "a" * 64 14 | KMD_ADDRESS = "http://localhost:4002" 15 | KMD_TOKEN = "a" * 64 16 | 17 | algod_client = algod.AlgodClient( 18 | algod_token=ALGOD_TOKEN, algod_address=ALGOD_ADDRESS 19 | ) 20 | kmd_client = KMDClient(kmd_token=KMD_TOKEN, kmd_address=KMD_ADDRESS) 21 | 22 | @classmethod 23 | @functools.lru_cache() 24 | def faucet(cls) -> Account: 25 | default_wallet_name = cls.kmd_client.list_wallets()[0]["name"] 26 | # Sandbox's wallet has no password 27 | wallet = Wallet(default_wallet_name, "", cls.kmd_client) 28 | 29 | for account in wallet.list_keys(): 30 | info = cls.algod_client.account_info(account) 31 | if ( 32 | info 33 | and info.get("status") == "Online" 34 | # and info.get("created-at-round", 0) == 0 # Needs the indexer. 35 | ): 36 | return Account( 37 | account, wallet.export_key(account), algod_client=cls.algod_client 38 | ) 39 | 40 | raise KeyError("Could not find sandbox faucet") 41 | 42 | @classmethod 43 | def from_public_key(cls, account: str): 44 | default_wallet_name = cls.kmd_client.list_wallets()[0]["name"] 45 | # Sandbox's wallet has no password 46 | wallet = Wallet(default_wallet_name, "", cls.kmd_client) 47 | return Account( 48 | account, wallet.export_key(account), algod_client=cls.algod_client 49 | ) 50 | 51 | @classmethod 52 | def create(cls, funds_amount: int) -> Account: 53 | new_account = Account.create(algod_client=cls.algod_client) 54 | cls.faucet().pay(new_account, funds_amount) 55 | return new_account 56 | 57 | 58 | def generate_blocks(account: Account, num_blocks: int): 59 | for _ in range(num_blocks): 60 | account.pay(account, 0) 61 | 62 | 63 | def wait_until_round(algod_client: algod.AlgodClient, round: int, verbose=True): 64 | """Returns right after round `round` happened""" 65 | if verbose: 66 | print(f" --- ⏲️ Waiting until round: {round}.") 67 | if (delta := round - get_last_round(algod_client)) > 0: 68 | generate_blocks(Sandbox.faucet(), delta) 69 | 70 | 71 | def wait_until_ts(algod_client: algod.AlgodClient, timestamp: int, verbose=True): 72 | if verbose: 73 | print( 74 | f" --- ⏲️ Waiting until ts: {timestamp}.", 75 | f"(expected wait: {timestamp - get_last_timestamp(algod_client)}s)", 76 | ) 77 | old_ts = 0 78 | while (curr_ts := get_last_timestamp(algod_client)) < timestamp: 79 | generate_blocks(Sandbox.faucet(), 1) 80 | # If delta >= 25 we are still catching up, no need to sleep 81 | if curr_ts - old_ts < 25: 82 | time.sleep(1) 83 | old_ts = curr_ts 84 | -------------------------------------------------------------------------------- /smart_asa_abi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Smart ASA ref. implementation", 3 | "methods": [ 4 | { 5 | "name": "asset_app_optin", 6 | "args": [ 7 | { 8 | "type": "asset", 9 | "name": "asset", 10 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 11 | }, 12 | { 13 | "type": "axfer", 14 | "name": "underlying_asa_optin", 15 | "desc": "Underlying ASA opt-in transaction." 16 | } 17 | ], 18 | "returns": { 19 | "type": "void" 20 | }, 21 | "desc": "Smart ASA atomic opt-in to Smart ASA App and Underlying ASA." 22 | }, 23 | { 24 | "name": "asset_create", 25 | "args": [ 26 | { 27 | "type": "uint64", 28 | "name": "total", 29 | "desc": "The total number of base units of the Smart ASA to create." 30 | }, 31 | { 32 | "type": "uint32", 33 | "name": "decimals", 34 | "desc": "The number of digits to use after the decimal point when displaying the Smart ASA. If 0, the Smart ASA is not divisible." 35 | }, 36 | { 37 | "type": "bool", 38 | "name": "default_frozen", 39 | "desc": "Smart ASA default frozen status (True to freeze holdings by default)." 40 | }, 41 | { 42 | "type": "string", 43 | "name": "unit_name", 44 | "desc": "The name of a unit of Smart ASA." 45 | }, 46 | { 47 | "type": "string", 48 | "name": "name", 49 | "desc": "The name of the Smart ASA." 50 | }, 51 | { 52 | "type": "string", 53 | "name": "url", 54 | "desc": "Smart ASA external URL." 55 | }, 56 | { 57 | "type": "byte[]", 58 | "name": "metadata_hash", 59 | "desc": "Smart ASA metadata hash (suggested 32 bytes hash)." 60 | }, 61 | { 62 | "type": "address", 63 | "name": "manager_addr", 64 | "desc": "The address of the account that can manage the configuration of the Smart ASA and destroy it." 65 | }, 66 | { 67 | "type": "address", 68 | "name": "reserve_addr", 69 | "desc": "The address of the account that holds the reserve (non-minted) units of the asset and can mint or burn units of Smart ASA." 70 | }, 71 | { 72 | "type": "address", 73 | "name": "freeze_addr", 74 | "desc": "The address of the account that can freeze/unfreeze holdings of this Smart ASA globally or locally (specific accounts). If empty, freezing is not permitted." 75 | }, 76 | { 77 | "type": "address", 78 | "name": "clawback_addr", 79 | "desc": "The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted." 80 | } 81 | ], 82 | "returns": { 83 | "type": "uint64", 84 | "desc": "New Smart ASA ID." 85 | }, 86 | "desc": "Create a Smart ASA (triggers inner creation of an Underlying ASA)." 87 | }, 88 | { 89 | "name": "asset_config", 90 | "args": [ 91 | { 92 | "type": "asset", 93 | "name": "config_asset", 94 | "desc": "Underlying ASA ID to configure (ref. App Global State: \"smart_asa_id\")." 95 | }, 96 | { 97 | "type": "uint64", 98 | "name": "total", 99 | "desc": "The total number of base units of the Smart ASA to create. It can not be configured to less than its current circulating supply." 100 | }, 101 | { 102 | "type": "uint32", 103 | "name": "decimals", 104 | "desc": "The number of digits to use after the decimal point when displaying the Smart ASA. If 0, the Smart ASA is not divisible." 105 | }, 106 | { 107 | "type": "bool", 108 | "name": "default_frozen", 109 | "desc": "Smart ASA default frozen status (True to freeze holdings by default)." 110 | }, 111 | { 112 | "type": "string", 113 | "name": "unit_name", 114 | "desc": "The name of a unit of Smart ASA." 115 | }, 116 | { 117 | "type": "string", 118 | "name": "name", 119 | "desc": "The name of the Smart ASA." 120 | }, 121 | { 122 | "type": "string", 123 | "name": "url", 124 | "desc": "Smart ASA external URL." 125 | }, 126 | { 127 | "type": "byte[]", 128 | "name": "metadata_hash", 129 | "desc": "Smart ASA metadata hash (suggested 32 bytes hash)." 130 | }, 131 | { 132 | "type": "address", 133 | "name": "manager_addr", 134 | "desc": "The address of the account that can manage the configuration of the Smart ASA and destroy it." 135 | }, 136 | { 137 | "type": "address", 138 | "name": "reserve_addr", 139 | "desc": "The address of the account that holds the reserve (non-minted) units of the asset and can mint or burn units of Smart ASA." 140 | }, 141 | { 142 | "type": "address", 143 | "name": "freeze_addr", 144 | "desc": "The address of the account that can freeze/unfreeze holdings of this Smart ASA globally or locally (specific accounts). If empty, freezing is not permitted." 145 | }, 146 | { 147 | "type": "address", 148 | "name": "clawback_addr", 149 | "desc": "The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted." 150 | } 151 | ], 152 | "returns": { 153 | "type": "void" 154 | }, 155 | "desc": "Configure the Smart ASA. Use existing values for unchanged parameters. Setting Smart ASA roles to zero-address is irreversible." 156 | }, 157 | { 158 | "name": "asset_transfer", 159 | "args": [ 160 | { 161 | "type": "asset", 162 | "name": "xfer_asset", 163 | "desc": "Underlying ASA ID to transfer (ref. App Global State: \"smart_asa_id\")." 164 | }, 165 | { 166 | "type": "uint64", 167 | "name": "asset_amount", 168 | "desc": "Smart ASA amount to transfer." 169 | }, 170 | { 171 | "type": "account", 172 | "name": "asset_sender", 173 | "desc": "Smart ASA sender, for regular transfer this must be equal to the Smart ASA App caller." 174 | }, 175 | { 176 | "type": "account", 177 | "name": "asset_receiver", 178 | "desc": "The recipient of the Smart ASA transfer." 179 | } 180 | ], 181 | "returns": { 182 | "type": "void" 183 | }, 184 | "desc": "Smart ASA transfers: regular, clawback (Clawback Address), mint or burn (Reserve Address)." 185 | }, 186 | { 187 | "name": "asset_freeze", 188 | "args": [ 189 | { 190 | "type": "asset", 191 | "name": "freeze_asset", 192 | "desc": "Underlying ASA ID to freeze/unfreeze (ref. App Global State: \"smart_asa_id\")." 193 | }, 194 | { 195 | "type": "bool", 196 | "name": "asset_frozen", 197 | "desc": "Smart ASA ID forzen status." 198 | } 199 | ], 200 | "returns": { 201 | "type": "void" 202 | }, 203 | "desc": "Smart ASA global freeze (all accounts), called by the Freeze Address." 204 | }, 205 | { 206 | "name": "account_freeze", 207 | "args": [ 208 | { 209 | "type": "asset", 210 | "name": "freeze_asset", 211 | "desc": "Underlying ASA ID to freeze/unfreeze (ref. App Global State: \"smart_asa_id\")." 212 | }, 213 | { 214 | "type": "account", 215 | "name": "freeze_account", 216 | "desc": "Account to freeze/unfreeze." 217 | }, 218 | { 219 | "type": "bool", 220 | "name": "asset_frozen", 221 | "desc": "Smart ASA ID forzen status." 222 | } 223 | ], 224 | "returns": { 225 | "type": "void" 226 | }, 227 | "desc": "Smart ASA local freeze (account specific), called by the Freeze Address." 228 | }, 229 | { 230 | "name": "asset_app_closeout", 231 | "args": [ 232 | { 233 | "type": "asset", 234 | "name": "close_asset", 235 | "desc": "Underlying ASA ID to close-out (ref. App Global State: \"smart_asa_id\")." 236 | }, 237 | { 238 | "type": "account", 239 | "name": "close_to", 240 | "desc": "Account to send all Smart ASA reminder to. If the asset/account is forzen then this must be set to Smart ASA Creator." 241 | } 242 | ], 243 | "returns": { 244 | "type": "void" 245 | }, 246 | "desc": "Smart ASA atomic close-out of Smart ASA App and Underlying ASA." 247 | }, 248 | { 249 | "name": "asset_destroy", 250 | "args": [ 251 | { 252 | "type": "asset", 253 | "name": "destroy_asset", 254 | "desc": "Underlying ASA ID to destroy (ref. App Global State: \"smart_asa_id\")." 255 | } 256 | ], 257 | "returns": { 258 | "type": "void" 259 | }, 260 | "desc": "Destroy the Underlying ASA, must be called by Manager Address." 261 | }, 262 | { 263 | "name": "get_asset_is_frozen", 264 | "args": [ 265 | { 266 | "type": "asset", 267 | "name": "freeze_asset", 268 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 269 | } 270 | ], 271 | "returns": { 272 | "type": "bool", 273 | "desc": "Smart ASA global frozen status." 274 | }, 275 | "desc": "Get Smart ASA global frozen status." 276 | }, 277 | { 278 | "name": "get_account_is_frozen", 279 | "args": [ 280 | { 281 | "type": "asset", 282 | "name": "freeze_asset", 283 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 284 | }, 285 | { 286 | "type": "account", 287 | "name": "freeze_account", 288 | "desc": "Account to check." 289 | } 290 | ], 291 | "returns": { 292 | "type": "bool", 293 | "desc": "Smart ASA local frozen status (account specific)." 294 | }, 295 | "desc": "Get Smart ASA local frozen status (account specific)." 296 | }, 297 | { 298 | "name": "get_circulating_supply", 299 | "args": [ 300 | { 301 | "type": "asset", 302 | "name": "asset", 303 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 304 | } 305 | ], 306 | "returns": { 307 | "type": "uint64", 308 | "desc": "Smart ASA circulating supply." 309 | }, 310 | "desc": "Get Smart ASA circulating supply." 311 | }, 312 | { 313 | "name": "get_optin_min_balance", 314 | "args": [ 315 | { 316 | "type": "asset", 317 | "name": "asset", 318 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 319 | } 320 | ], 321 | "returns": { 322 | "type": "uint64", 323 | "desc": "Smart ASA required minimum balance in microALGO." 324 | }, 325 | "desc": "Get Smart ASA required minimum balance (including Underlying ASA and App Local State)." 326 | }, 327 | { 328 | "name": "get_asset_config", 329 | "args": [ 330 | { 331 | "type": "asset", 332 | "name": "asset", 333 | "desc": "Underlying ASA ID (ref. App Global State: \"smart_asa_id\")." 334 | } 335 | ], 336 | "returns": { 337 | "type": "(uint64,uint32,bool,string,string,string,byte[],address,address,address,address)", 338 | "desc": "Smart ASA configuration parameters." 339 | }, 340 | "desc": "Get Smart ASA configuration." 341 | } 342 | ], 343 | "networks": {} 344 | } 345 | -------------------------------------------------------------------------------- /smart_asa_approval.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | intcblock 0 1 8 4 65536 18446744073709551615 3 | bytecblock 0x736d6172745f6173615f6964 0x66726f7a656e 0x726573657276655f61646472 0x667265657a655f61646472 0x636c61776261636b5f61646472 0x151f7c75 0x6d616e616765725f61646472 0x746f74616c 0x64656661756c745f66726f7a656e 0x 0x646563696d616c73 0x756e69745f6e616d65 0x6e616d65 0x75726c 0x6d657461646174615f68617368 0x00 4 | txn NumAppArgs 5 | intc_0 // 0 6 | == 7 | bnz main_l28 8 | txna ApplicationArgs 0 9 | pushbytes 0xf80f5591 // "asset_app_optin(asset,axfer)void" 10 | == 11 | bnz main_l27 12 | txna ApplicationArgs 0 13 | pushbytes 0xe7ecd5a8 // "asset_create(uint64,uint32,bool,string,string,string,byte[],address,address,address,address)uint64" 14 | == 15 | bnz main_l26 16 | txna ApplicationArgs 0 17 | pushbytes 0xee6a84aa // "asset_config(asset,uint64,uint32,bool,string,string,string,byte[],address,address,address,address)void" 18 | == 19 | bnz main_l25 20 | txna ApplicationArgs 0 21 | pushbytes 0x2fc743a8 // "asset_transfer(asset,uint64,account,account)void" 22 | == 23 | bnz main_l24 24 | txna ApplicationArgs 0 25 | pushbytes 0x15cf2ba3 // "asset_freeze(asset,bool)void" 26 | == 27 | bnz main_l23 28 | txna ApplicationArgs 0 29 | pushbytes 0x7b351ce5 // "account_freeze(asset,account,bool)void" 30 | == 31 | bnz main_l22 32 | txna ApplicationArgs 0 33 | pushbytes 0x7dfcf38c // "asset_app_closeout(asset,account)void" 34 | == 35 | bnz main_l21 36 | txna ApplicationArgs 0 37 | pushbytes 0x4b17bf20 // "asset_destroy(asset)void" 38 | == 39 | bnz main_l20 40 | txna ApplicationArgs 0 41 | pushbytes 0x127fb717 // "get_asset_is_frozen(asset)bool" 42 | == 43 | bnz main_l19 44 | txna ApplicationArgs 0 45 | pushbytes 0x026f8a9d // "get_account_is_frozen(asset,account)bool" 46 | == 47 | bnz main_l18 48 | txna ApplicationArgs 0 49 | pushbytes 0xe97483bf // "get_circulating_supply(asset)uint64" 50 | == 51 | bnz main_l17 52 | txna ApplicationArgs 0 53 | pushbytes 0x4b8f8cf9 // "get_optin_min_balance(asset)uint64" 54 | == 55 | bnz main_l16 56 | txna ApplicationArgs 0 57 | pushbytes 0xce2f05f3 // "get_asset_config(asset)(uint64,uint32,bool,string,string,string,byte[],address,address,address,address)" 58 | == 59 | bnz main_l15 60 | err 61 | main_l15: 62 | txn OnCompletion 63 | intc_0 // NoOp 64 | == 65 | txn ApplicationID 66 | intc_0 // 0 67 | != 68 | && 69 | assert 70 | txna ApplicationArgs 1 71 | intc_0 // 0 72 | getbyte 73 | callsub getassetconfig_24 74 | store 56 75 | bytec 5 // 0x151f7c75 76 | load 56 77 | concat 78 | log 79 | intc_1 // 1 80 | return 81 | main_l16: 82 | txn OnCompletion 83 | intc_0 // NoOp 84 | == 85 | txn ApplicationID 86 | intc_0 // 0 87 | != 88 | && 89 | assert 90 | txna ApplicationArgs 1 91 | intc_0 // 0 92 | getbyte 93 | callsub getoptinminbalance_23 94 | store 55 95 | bytec 5 // 0x151f7c75 96 | load 55 97 | itob 98 | concat 99 | log 100 | intc_1 // 1 101 | return 102 | main_l17: 103 | txn OnCompletion 104 | intc_0 // NoOp 105 | == 106 | txn ApplicationID 107 | intc_0 // 0 108 | != 109 | && 110 | assert 111 | txna ApplicationArgs 1 112 | intc_0 // 0 113 | getbyte 114 | callsub getcirculatingsupply_22 115 | store 53 116 | bytec 5 // 0x151f7c75 117 | load 53 118 | itob 119 | concat 120 | log 121 | intc_1 // 1 122 | return 123 | main_l18: 124 | txn OnCompletion 125 | intc_0 // NoOp 126 | == 127 | txn ApplicationID 128 | intc_0 // 0 129 | != 130 | && 131 | assert 132 | txna ApplicationArgs 1 133 | intc_0 // 0 134 | getbyte 135 | store 49 136 | txna ApplicationArgs 2 137 | intc_0 // 0 138 | getbyte 139 | store 50 140 | load 49 141 | load 50 142 | callsub getaccountisfrozen_21 143 | store 51 144 | bytec 5 // 0x151f7c75 145 | bytec 15 // 0x00 146 | intc_0 // 0 147 | load 51 148 | setbit 149 | concat 150 | log 151 | intc_1 // 1 152 | return 153 | main_l19: 154 | txn OnCompletion 155 | intc_0 // NoOp 156 | == 157 | txn ApplicationID 158 | intc_0 // 0 159 | != 160 | && 161 | assert 162 | txna ApplicationArgs 1 163 | intc_0 // 0 164 | getbyte 165 | callsub getassetisfrozen_20 166 | store 48 167 | bytec 5 // 0x151f7c75 168 | bytec 15 // 0x00 169 | intc_0 // 0 170 | load 48 171 | setbit 172 | concat 173 | log 174 | intc_1 // 1 175 | return 176 | main_l20: 177 | txn OnCompletion 178 | intc_0 // NoOp 179 | == 180 | txn ApplicationID 181 | intc_0 // 0 182 | != 183 | && 184 | assert 185 | txna ApplicationArgs 1 186 | intc_0 // 0 187 | getbyte 188 | callsub assetdestroy_19 189 | intc_1 // 1 190 | return 191 | main_l21: 192 | txn OnCompletion 193 | pushint 2 // CloseOut 194 | == 195 | assert 196 | txna ApplicationArgs 1 197 | intc_0 // 0 198 | getbyte 199 | store 46 200 | txna ApplicationArgs 2 201 | intc_0 // 0 202 | getbyte 203 | store 47 204 | load 46 205 | load 47 206 | callsub assetappcloseout_18 207 | intc_1 // 1 208 | return 209 | main_l22: 210 | txn OnCompletion 211 | intc_0 // NoOp 212 | == 213 | txn ApplicationID 214 | intc_0 // 0 215 | != 216 | && 217 | assert 218 | txna ApplicationArgs 1 219 | intc_0 // 0 220 | getbyte 221 | store 43 222 | txna ApplicationArgs 2 223 | intc_0 // 0 224 | getbyte 225 | store 44 226 | txna ApplicationArgs 3 227 | intc_0 // 0 228 | intc_2 // 8 229 | * 230 | getbit 231 | store 45 232 | load 43 233 | load 44 234 | load 45 235 | callsub accountfreeze_17 236 | intc_1 // 1 237 | return 238 | main_l23: 239 | txn OnCompletion 240 | intc_0 // NoOp 241 | == 242 | txn ApplicationID 243 | intc_0 // 0 244 | != 245 | && 246 | assert 247 | txna ApplicationArgs 1 248 | intc_0 // 0 249 | getbyte 250 | store 41 251 | txna ApplicationArgs 2 252 | intc_0 // 0 253 | intc_2 // 8 254 | * 255 | getbit 256 | store 42 257 | load 41 258 | load 42 259 | callsub assetfreeze_16 260 | intc_1 // 1 261 | return 262 | main_l24: 263 | txn OnCompletion 264 | intc_0 // NoOp 265 | == 266 | txn ApplicationID 267 | intc_0 // 0 268 | != 269 | && 270 | assert 271 | txna ApplicationArgs 1 272 | intc_0 // 0 273 | getbyte 274 | store 37 275 | txna ApplicationArgs 2 276 | btoi 277 | store 38 278 | txna ApplicationArgs 3 279 | intc_0 // 0 280 | getbyte 281 | store 39 282 | txna ApplicationArgs 4 283 | intc_0 // 0 284 | getbyte 285 | store 40 286 | load 37 287 | load 38 288 | load 39 289 | load 40 290 | callsub assettransfer_15 291 | intc_1 // 1 292 | return 293 | main_l25: 294 | txn OnCompletion 295 | intc_0 // NoOp 296 | == 297 | txn ApplicationID 298 | intc_0 // 0 299 | != 300 | && 301 | assert 302 | txna ApplicationArgs 1 303 | intc_0 // 0 304 | getbyte 305 | store 25 306 | txna ApplicationArgs 2 307 | btoi 308 | store 26 309 | txna ApplicationArgs 3 310 | intc_0 // 0 311 | extract_uint32 312 | store 27 313 | txna ApplicationArgs 4 314 | intc_0 // 0 315 | intc_2 // 8 316 | * 317 | getbit 318 | store 28 319 | txna ApplicationArgs 5 320 | store 29 321 | txna ApplicationArgs 6 322 | store 30 323 | txna ApplicationArgs 7 324 | store 31 325 | txna ApplicationArgs 8 326 | store 32 327 | txna ApplicationArgs 9 328 | store 33 329 | txna ApplicationArgs 10 330 | store 34 331 | txna ApplicationArgs 11 332 | store 35 333 | txna ApplicationArgs 12 334 | store 36 335 | load 25 336 | load 26 337 | load 27 338 | load 28 339 | load 29 340 | load 30 341 | load 31 342 | load 32 343 | load 33 344 | load 34 345 | load 35 346 | load 36 347 | callsub assetconfig_14 348 | intc_1 // 1 349 | return 350 | main_l26: 351 | txn OnCompletion 352 | intc_0 // NoOp 353 | == 354 | txn ApplicationID 355 | intc_0 // 0 356 | != 357 | && 358 | assert 359 | txna ApplicationArgs 1 360 | btoi 361 | store 2 362 | txna ApplicationArgs 2 363 | intc_0 // 0 364 | extract_uint32 365 | store 3 366 | txna ApplicationArgs 3 367 | intc_0 // 0 368 | intc_2 // 8 369 | * 370 | getbit 371 | store 4 372 | txna ApplicationArgs 4 373 | store 5 374 | txna ApplicationArgs 5 375 | store 6 376 | txna ApplicationArgs 6 377 | store 7 378 | txna ApplicationArgs 7 379 | store 8 380 | txna ApplicationArgs 8 381 | store 9 382 | txna ApplicationArgs 9 383 | store 10 384 | txna ApplicationArgs 10 385 | store 11 386 | txna ApplicationArgs 11 387 | store 12 388 | load 2 389 | load 3 390 | load 4 391 | load 5 392 | load 6 393 | load 7 394 | load 8 395 | load 9 396 | load 10 397 | load 11 398 | load 12 399 | callsub assetcreate_13 400 | store 13 401 | bytec 5 // 0x151f7c75 402 | load 13 403 | itob 404 | concat 405 | log 406 | intc_1 // 1 407 | return 408 | main_l27: 409 | txn OnCompletion 410 | intc_1 // OptIn 411 | == 412 | assert 413 | txna ApplicationArgs 1 414 | intc_0 // 0 415 | getbyte 416 | store 0 417 | txn GroupIndex 418 | intc_1 // 1 419 | - 420 | store 1 421 | load 1 422 | gtxns TypeEnum 423 | intc_3 // axfer 424 | == 425 | assert 426 | load 0 427 | load 1 428 | callsub assetappoptin_12 429 | intc_1 // 1 430 | return 431 | main_l28: 432 | txn OnCompletion 433 | intc_0 // NoOp 434 | == 435 | bnz main_l34 436 | txn OnCompletion 437 | intc_3 // UpdateApplication 438 | == 439 | bnz main_l33 440 | txn OnCompletion 441 | pushint 5 // DeleteApplication 442 | == 443 | bnz main_l32 444 | err 445 | main_l32: 446 | intc_0 // 0 447 | return 448 | main_l33: 449 | intc_0 // 0 450 | return 451 | main_l34: 452 | txn ApplicationID 453 | intc_0 // 0 454 | == 455 | assert 456 | callsub assetappcreate_11 457 | intc_1 // 1 458 | return 459 | 460 | // init_global_state 461 | initglobalstate_0: 462 | bytec_0 // "smart_asa_id" 463 | intc_0 // 0 464 | app_global_put 465 | bytec 7 // "total" 466 | intc_0 // 0 467 | app_global_put 468 | bytec 10 // "decimals" 469 | intc_0 // 0 470 | app_global_put 471 | bytec 8 // "default_frozen" 472 | intc_0 // 0 473 | app_global_put 474 | bytec 11 // "unit_name" 475 | bytec 9 // "" 476 | app_global_put 477 | bytec 12 // "name" 478 | bytec 9 // "" 479 | app_global_put 480 | bytec 13 // "url" 481 | bytec 9 // "" 482 | app_global_put 483 | bytec 14 // "metadata_hash" 484 | bytec 9 // "" 485 | app_global_put 486 | bytec 6 // "manager_addr" 487 | global ZeroAddress 488 | app_global_put 489 | bytec_2 // "reserve_addr" 490 | global ZeroAddress 491 | app_global_put 492 | bytec_3 // "freeze_addr" 493 | global ZeroAddress 494 | app_global_put 495 | bytec 4 // "clawback_addr" 496 | global ZeroAddress 497 | app_global_put 498 | bytec_1 // "frozen" 499 | intc_0 // 0 500 | app_global_put 501 | retsub 502 | 503 | // init_local_state 504 | initlocalstate_1: 505 | txn Sender 506 | bytec_0 // "smart_asa_id" 507 | bytec_0 // "smart_asa_id" 508 | app_global_get 509 | app_local_put 510 | txn Sender 511 | bytec_1 // "frozen" 512 | intc_0 // 0 513 | app_local_put 514 | retsub 515 | 516 | // digit_to_ascii 517 | digittoascii_2: 518 | store 78 519 | pushbytes 0x30313233343536373839 // "0123456789" 520 | load 78 521 | intc_1 // 1 522 | extract3 523 | retsub 524 | 525 | // itoa 526 | itoa_3: 527 | store 77 528 | load 77 529 | intc_0 // 0 530 | == 531 | bnz itoa_3_l5 532 | load 77 533 | pushint 10 // 10 534 | / 535 | intc_0 // 0 536 | > 537 | bnz itoa_3_l4 538 | bytec 9 // "" 539 | itoa_3_l3: 540 | load 77 541 | pushint 10 // 10 542 | % 543 | callsub digittoascii_2 544 | concat 545 | b itoa_3_l6 546 | itoa_3_l4: 547 | load 77 548 | pushint 10 // 10 549 | / 550 | load 77 551 | swap 552 | callsub itoa_3 553 | swap 554 | store 77 555 | b itoa_3_l3 556 | itoa_3_l5: 557 | pushbytes 0x30 // "0" 558 | itoa_3_l6: 559 | retsub 560 | 561 | // strip_len_prefix 562 | striplenprefix_4: 563 | extract 2 0 564 | retsub 565 | 566 | // underlying_asa_create_inner_tx 567 | underlyingasacreateinnertx_5: 568 | itxn_begin 569 | intc_0 // 0 570 | itxn_field Fee 571 | pushint 3 // acfg 572 | itxn_field TypeEnum 573 | intc 5 // 18446744073709551615 574 | itxn_field ConfigAssetTotal 575 | intc_0 // 0 576 | itxn_field ConfigAssetDecimals 577 | intc_1 // 1 578 | itxn_field ConfigAssetDefaultFrozen 579 | pushbytes 0x532d415341 // "S-ASA" 580 | itxn_field ConfigAssetUnitName 581 | pushbytes 0x534d4152542d415341 // "SMART-ASA" 582 | itxn_field ConfigAssetName 583 | pushbytes 0x736d6172742d6173612d6170702d69643a // "smart-asa-app-id:" 584 | global CurrentApplicationID 585 | callsub itoa_3 586 | concat 587 | itxn_field ConfigAssetURL 588 | global CurrentApplicationAddress 589 | itxn_field ConfigAssetManager 590 | global CurrentApplicationAddress 591 | itxn_field ConfigAssetReserve 592 | global CurrentApplicationAddress 593 | itxn_field ConfigAssetFreeze 594 | global CurrentApplicationAddress 595 | itxn_field ConfigAssetClawback 596 | itxn_submit 597 | itxn CreatedAssetID 598 | retsub 599 | 600 | // smart_asa_transfer_inner_txn 601 | smartasatransferinnertxn_6: 602 | store 101 603 | store 100 604 | store 99 605 | store 98 606 | itxn_begin 607 | intc_0 // 0 608 | itxn_field Fee 609 | intc_3 // axfer 610 | itxn_field TypeEnum 611 | load 98 612 | itxn_field XferAsset 613 | load 99 614 | itxn_field AssetAmount 615 | load 100 616 | itxn_field AssetSender 617 | load 101 618 | itxn_field AssetReceiver 619 | itxn_submit 620 | retsub 621 | 622 | // smart_asa_destroy_inner_txn 623 | smartasadestroyinnertxn_7: 624 | store 114 625 | itxn_begin 626 | intc_0 // 0 627 | itxn_field Fee 628 | pushint 3 // acfg 629 | itxn_field TypeEnum 630 | load 114 631 | itxn_field ConfigAsset 632 | itxn_submit 633 | retsub 634 | 635 | // is_valid_address_bytes_length 636 | isvalidaddressbyteslength_8: 637 | len 638 | pushint 32 // 32 639 | == 640 | // Invalid Address length (must be 32 bytes) 641 | assert 642 | retsub 643 | 644 | // circulating_supply 645 | circulatingsupply_9: 646 | store 91 647 | global CurrentApplicationAddress 648 | load 91 649 | asset_holding_get AssetBalance 650 | store 93 651 | store 92 652 | intc 5 // 18446744073709551615 653 | load 92 654 | - 655 | retsub 656 | 657 | // getter_preconditions 658 | getterpreconditions_10: 659 | store 115 660 | bytec_0 // "smart_asa_id" 661 | app_global_get 662 | // Smart ASA ID dose not exist 663 | assert 664 | bytec_0 // "smart_asa_id" 665 | app_global_get 666 | load 115 667 | == 668 | // Invalid Smart ASA ID 669 | assert 670 | retsub 671 | 672 | // asset_app_create 673 | assetappcreate_11: 674 | txn GlobalNumUint 675 | pushint 5 // 5 676 | == 677 | // Wrong State Schema - Expexted Global Ints: 5 678 | assert 679 | txn GlobalNumByteSlice 680 | intc_2 // 8 681 | == 682 | // Wrong State Schema - Expexted Global Bytes: 8 683 | assert 684 | txn LocalNumUint 685 | pushint 2 // 2 686 | == 687 | // Wrong State Schema - Expexted Local Ints: 2 688 | assert 689 | txn LocalNumByteSlice 690 | intc_0 // 0 691 | == 692 | // Wrong State Schema - Expexted Local Bytes: 0 693 | assert 694 | callsub initglobalstate_0 695 | intc_1 // 1 696 | return 697 | 698 | // asset_app_optin 699 | assetappoptin_12: 700 | store 74 701 | store 73 702 | bytec_0 // "smart_asa_id" 703 | app_global_get 704 | // Smart ASA ID dose not exist 705 | assert 706 | bytec_0 // "smart_asa_id" 707 | app_global_get 708 | load 73 709 | txnas Assets 710 | == 711 | // Invalid Smart ASA ID 712 | assert 713 | load 74 714 | gtxns TypeEnum 715 | intc_3 // axfer 716 | == 717 | // Reference Opt-In Txn: Wrong Txn Type (Expected: Axfer) 718 | assert 719 | load 74 720 | gtxns XferAsset 721 | bytec_0 // "smart_asa_id" 722 | app_global_get 723 | == 724 | // Reference Opt-In Txn: Wrong Asset ID (Expected: Smart ASA ID) 725 | assert 726 | load 74 727 | gtxns Sender 728 | txn Sender 729 | == 730 | // Reference Opt-In Txn: Wrong Sender (Expected: App Caller) 731 | assert 732 | load 74 733 | gtxns AssetReceiver 734 | txn Sender 735 | == 736 | // Reference Opt-In Txn: Wrong Asset Receiver (Expected: App Caller) 737 | assert 738 | load 74 739 | gtxns AssetAmount 740 | intc_0 // 0 741 | == 742 | // Reference Opt-In Txn: Wrong Asset Amount (Expected: 0) 743 | assert 744 | load 74 745 | gtxns AssetCloseTo 746 | global ZeroAddress 747 | == 748 | // Reference Opt-In Txn: Wrong Asset CloseTo (Expected: Zero Address) 749 | assert 750 | txn Sender 751 | load 73 752 | txnas Assets 753 | asset_holding_get AssetBalance 754 | store 76 755 | store 75 756 | load 76 757 | // Missing Opt-In to Underlying ASA 758 | assert 759 | callsub initlocalstate_1 760 | bytec 8 // "default_frozen" 761 | app_global_get 762 | load 75 763 | intc_0 // 0 764 | > 765 | || 766 | bz assetappoptin_12_l2 767 | txn Sender 768 | bytec_1 // "frozen" 769 | intc_1 // 1 770 | app_local_put 771 | assetappoptin_12_l2: 772 | intc_1 // 1 773 | return 774 | 775 | // asset_create 776 | assetcreate_13: 777 | store 24 778 | store 23 779 | store 22 780 | store 21 781 | store 20 782 | store 19 783 | store 18 784 | store 17 785 | store 16 786 | store 15 787 | store 14 788 | txn Sender 789 | global CreatorAddress 790 | == 791 | // Caller not authorized (must be: App Creator Address) 792 | assert 793 | bytec_0 // "smart_asa_id" 794 | app_global_get 795 | ! 796 | // Smart ASA ID already exists 797 | assert 798 | load 21 799 | callsub isvalidaddressbyteslength_8 800 | load 22 801 | callsub isvalidaddressbyteslength_8 802 | load 23 803 | callsub isvalidaddressbyteslength_8 804 | load 24 805 | callsub isvalidaddressbyteslength_8 806 | bytec_0 // "smart_asa_id" 807 | callsub underlyingasacreateinnertx_5 808 | app_global_put 809 | bytec 7 // "total" 810 | load 14 811 | app_global_put 812 | bytec 10 // "decimals" 813 | load 15 814 | app_global_put 815 | bytec 8 // "default_frozen" 816 | load 16 817 | app_global_put 818 | bytec 11 // "unit_name" 819 | load 17 820 | extract 2 0 821 | app_global_put 822 | bytec 12 // "name" 823 | load 18 824 | extract 2 0 825 | app_global_put 826 | bytec 13 // "url" 827 | load 19 828 | extract 2 0 829 | app_global_put 830 | bytec 14 // "metadata_hash" 831 | load 20 832 | callsub striplenprefix_4 833 | app_global_put 834 | bytec 6 // "manager_addr" 835 | load 21 836 | app_global_put 837 | bytec_2 // "reserve_addr" 838 | load 22 839 | app_global_put 840 | bytec_3 // "freeze_addr" 841 | load 23 842 | app_global_put 843 | bytec 4 // "clawback_addr" 844 | load 24 845 | app_global_put 846 | bytec_0 // "smart_asa_id" 847 | app_global_get 848 | retsub 849 | 850 | // asset_config 851 | assetconfig_14: 852 | store 90 853 | store 89 854 | store 88 855 | store 87 856 | store 86 857 | store 85 858 | store 84 859 | store 83 860 | store 82 861 | store 81 862 | store 80 863 | store 79 864 | bytec_0 // "smart_asa_id" 865 | app_global_get 866 | // Smart ASA ID dose not exist 867 | assert 868 | bytec_0 // "smart_asa_id" 869 | app_global_get 870 | load 79 871 | txnas Assets 872 | == 873 | // Invalid Smart ASA ID 874 | assert 875 | load 87 876 | callsub isvalidaddressbyteslength_8 877 | load 88 878 | callsub isvalidaddressbyteslength_8 879 | load 89 880 | callsub isvalidaddressbyteslength_8 881 | load 90 882 | callsub isvalidaddressbyteslength_8 883 | txn Sender 884 | bytec 6 // "manager_addr" 885 | app_global_get 886 | == 887 | // Caller not authorized (must be: Manager Address) 888 | assert 889 | bytec_2 // "reserve_addr" 890 | app_global_get 891 | load 88 892 | != 893 | bnz assetconfig_14_l5 894 | assetconfig_14_l1: 895 | bytec_3 // "freeze_addr" 896 | app_global_get 897 | load 89 898 | != 899 | bnz assetconfig_14_l4 900 | assetconfig_14_l2: 901 | bytec 4 // "clawback_addr" 902 | app_global_get 903 | load 90 904 | != 905 | bz assetconfig_14_l6 906 | bytec 4 // "clawback_addr" 907 | app_global_get 908 | global ZeroAddress 909 | != 910 | // Clawback Address has been deleted 911 | assert 912 | b assetconfig_14_l6 913 | assetconfig_14_l4: 914 | bytec_3 // "freeze_addr" 915 | app_global_get 916 | global ZeroAddress 917 | != 918 | // Freeze Address has been deleted 919 | assert 920 | b assetconfig_14_l2 921 | assetconfig_14_l5: 922 | bytec_2 // "reserve_addr" 923 | app_global_get 924 | global ZeroAddress 925 | != 926 | // Reserve Address has been deleted 927 | assert 928 | b assetconfig_14_l1 929 | assetconfig_14_l6: 930 | load 80 931 | bytec_0 // "smart_asa_id" 932 | app_global_get 933 | callsub circulatingsupply_9 934 | >= 935 | // Invalid Total (must be >= Circulating Supply) 936 | assert 937 | bytec 7 // "total" 938 | load 80 939 | app_global_put 940 | bytec 10 // "decimals" 941 | load 81 942 | app_global_put 943 | bytec 8 // "default_frozen" 944 | load 82 945 | app_global_put 946 | bytec 11 // "unit_name" 947 | load 83 948 | extract 2 0 949 | app_global_put 950 | bytec 12 // "name" 951 | load 84 952 | extract 2 0 953 | app_global_put 954 | bytec 13 // "url" 955 | load 85 956 | extract 2 0 957 | app_global_put 958 | bytec 14 // "metadata_hash" 959 | load 86 960 | callsub striplenprefix_4 961 | app_global_put 962 | bytec 6 // "manager_addr" 963 | load 87 964 | app_global_put 965 | bytec_2 // "reserve_addr" 966 | load 88 967 | app_global_put 968 | bytec_3 // "freeze_addr" 969 | load 89 970 | app_global_put 971 | bytec 4 // "clawback_addr" 972 | load 90 973 | app_global_put 974 | retsub 975 | 976 | // asset_transfer 977 | assettransfer_15: 978 | store 97 979 | store 96 980 | store 95 981 | store 94 982 | bytec_0 // "smart_asa_id" 983 | app_global_get 984 | // Smart ASA ID dose not exist 985 | assert 986 | bytec_0 // "smart_asa_id" 987 | app_global_get 988 | load 94 989 | txnas Assets 990 | == 991 | // Invalid Smart ASA ID 992 | assert 993 | load 96 994 | txnas Accounts 995 | callsub isvalidaddressbyteslength_8 996 | load 97 997 | txnas Accounts 998 | callsub isvalidaddressbyteslength_8 999 | txn Sender 1000 | load 96 1001 | txnas Accounts 1002 | == 1003 | txn Sender 1004 | bytec 4 // "clawback_addr" 1005 | app_global_get 1006 | != 1007 | && 1008 | bnz assettransfer_15_l6 1009 | txn Sender 1010 | bytec_2 // "reserve_addr" 1011 | app_global_get 1012 | == 1013 | load 96 1014 | txnas Accounts 1015 | global CurrentApplicationAddress 1016 | == 1017 | && 1018 | bnz assettransfer_15_l5 1019 | txn Sender 1020 | bytec_2 // "reserve_addr" 1021 | app_global_get 1022 | == 1023 | load 96 1024 | txnas Accounts 1025 | bytec_2 // "reserve_addr" 1026 | app_global_get 1027 | == 1028 | && 1029 | load 97 1030 | txnas Accounts 1031 | global CurrentApplicationAddress 1032 | == 1033 | && 1034 | bnz assettransfer_15_l4 1035 | txn Sender 1036 | bytec 4 // "clawback_addr" 1037 | app_global_get 1038 | == 1039 | // Caller not authorized (must be: Clawback Address) 1040 | assert 1041 | bytec_0 // "smart_asa_id" 1042 | app_global_get 1043 | load 96 1044 | txnas Accounts 1045 | bytec_0 // "smart_asa_id" 1046 | app_local_get 1047 | == 1048 | bytec_0 // "smart_asa_id" 1049 | app_global_get 1050 | load 97 1051 | txnas Accounts 1052 | bytec_0 // "smart_asa_id" 1053 | app_local_get 1054 | == 1055 | && 1056 | // Invalid Smart ASA ID 1057 | assert 1058 | b assettransfer_15_l7 1059 | assettransfer_15_l4: 1060 | bytec_1 // "frozen" 1061 | app_global_get 1062 | ! 1063 | // Smart ASA is frozen 1064 | assert 1065 | load 96 1066 | txnas Accounts 1067 | bytec_1 // "frozen" 1068 | app_local_get 1069 | ! 1070 | // Sender is frozen 1071 | assert 1072 | bytec_0 // "smart_asa_id" 1073 | app_global_get 1074 | load 96 1075 | txnas Accounts 1076 | bytec_0 // "smart_asa_id" 1077 | app_local_get 1078 | == 1079 | // Invalid Smart ASA ID 1080 | assert 1081 | b assettransfer_15_l7 1082 | assettransfer_15_l5: 1083 | bytec_1 // "frozen" 1084 | app_global_get 1085 | ! 1086 | // Smart ASA is frozen 1087 | assert 1088 | load 97 1089 | txnas Accounts 1090 | bytec_1 // "frozen" 1091 | app_local_get 1092 | ! 1093 | // Receiver is frozen 1094 | assert 1095 | bytec_0 // "smart_asa_id" 1096 | app_global_get 1097 | load 97 1098 | txnas Accounts 1099 | bytec_0 // "smart_asa_id" 1100 | app_local_get 1101 | == 1102 | // Invalid Smart ASA ID 1103 | assert 1104 | bytec_0 // "smart_asa_id" 1105 | app_global_get 1106 | callsub circulatingsupply_9 1107 | load 95 1108 | + 1109 | bytec 7 // "total" 1110 | app_global_get 1111 | <= 1112 | // Over-minting (can not mint more than Total) 1113 | assert 1114 | b assettransfer_15_l7 1115 | assettransfer_15_l6: 1116 | bytec_1 // "frozen" 1117 | app_global_get 1118 | ! 1119 | // Smart ASA is frozen 1120 | assert 1121 | load 96 1122 | txnas Accounts 1123 | bytec_1 // "frozen" 1124 | app_local_get 1125 | ! 1126 | // Sender is frozen 1127 | assert 1128 | load 97 1129 | txnas Accounts 1130 | bytec_1 // "frozen" 1131 | app_local_get 1132 | ! 1133 | // Receiver is frozen 1134 | assert 1135 | bytec_0 // "smart_asa_id" 1136 | app_global_get 1137 | load 96 1138 | txnas Accounts 1139 | bytec_0 // "smart_asa_id" 1140 | app_local_get 1141 | == 1142 | bytec_0 // "smart_asa_id" 1143 | app_global_get 1144 | load 97 1145 | txnas Accounts 1146 | bytec_0 // "smart_asa_id" 1147 | app_local_get 1148 | == 1149 | && 1150 | // Invalid Smart ASA ID 1151 | assert 1152 | assettransfer_15_l7: 1153 | load 94 1154 | txnas Assets 1155 | load 95 1156 | load 96 1157 | txnas Accounts 1158 | load 97 1159 | txnas Accounts 1160 | callsub smartasatransferinnertxn_6 1161 | retsub 1162 | 1163 | // asset_freeze 1164 | assetfreeze_16: 1165 | store 103 1166 | store 102 1167 | bytec_0 // "smart_asa_id" 1168 | app_global_get 1169 | // Smart ASA ID dose not exist 1170 | assert 1171 | bytec_0 // "smart_asa_id" 1172 | app_global_get 1173 | load 102 1174 | txnas Assets 1175 | == 1176 | // Invalid Smart ASA ID 1177 | assert 1178 | txn Sender 1179 | bytec_3 // "freeze_addr" 1180 | app_global_get 1181 | == 1182 | // Caller not authorized (must be: Freeze Address) 1183 | assert 1184 | bytec_1 // "frozen" 1185 | load 103 1186 | app_global_put 1187 | retsub 1188 | 1189 | // account_freeze 1190 | accountfreeze_17: 1191 | store 106 1192 | store 105 1193 | store 104 1194 | load 105 1195 | txnas Accounts 1196 | callsub isvalidaddressbyteslength_8 1197 | bytec_0 // "smart_asa_id" 1198 | app_global_get 1199 | // Smart ASA ID dose not exist 1200 | assert 1201 | bytec_0 // "smart_asa_id" 1202 | app_global_get 1203 | load 104 1204 | txnas Assets 1205 | == 1206 | // Invalid Smart ASA ID 1207 | assert 1208 | txn Sender 1209 | bytec_3 // "freeze_addr" 1210 | app_global_get 1211 | == 1212 | // Caller not authorized (must be: Freeze Address) 1213 | assert 1214 | load 105 1215 | txnas Accounts 1216 | bytec_1 // "frozen" 1217 | load 106 1218 | app_local_put 1219 | retsub 1220 | 1221 | // asset_app_closeout 1222 | assetappcloseout_18: 1223 | store 108 1224 | store 107 1225 | load 108 1226 | txnas Accounts 1227 | callsub isvalidaddressbyteslength_8 1228 | txn Sender 1229 | bytec_0 // "smart_asa_id" 1230 | app_local_get 1231 | load 107 1232 | txnas Assets 1233 | == 1234 | // Invalid Smart ASA ID 1235 | assert 1236 | global GroupSize 1237 | txn GroupIndex 1238 | intc_1 // 1 1239 | + 1240 | > 1241 | // Smart ASA CloseOut: Wrong group size (Expected: 2) 1242 | assert 1243 | txn GroupIndex 1244 | intc_1 // 1 1245 | + 1246 | gtxns TypeEnum 1247 | intc_3 // axfer 1248 | == 1249 | // Underlying ASA CloseOut Txn: Wrong Txn type (Expected: Axfer) 1250 | assert 1251 | txn GroupIndex 1252 | intc_1 // 1 1253 | + 1254 | gtxns XferAsset 1255 | load 107 1256 | txnas Assets 1257 | == 1258 | // Underlying ASA CloseOut Txn: Wrong ASA ID (Expected: Smart ASA ID) 1259 | assert 1260 | txn GroupIndex 1261 | intc_1 // 1 1262 | + 1263 | gtxns Sender 1264 | txn Sender 1265 | == 1266 | // Underlying ASA CloseOut Txn: Wrong sender (Expected: Smart ASA CloseOut caller) 1267 | assert 1268 | txn GroupIndex 1269 | intc_1 // 1 1270 | + 1271 | gtxns AssetAmount 1272 | intc_0 // 0 1273 | == 1274 | // Underlying ASA CloseOut Txn: Wrong amount (Expected: 0) 1275 | assert 1276 | txn GroupIndex 1277 | intc_1 // 1 1278 | + 1279 | gtxns AssetCloseTo 1280 | global CurrentApplicationAddress 1281 | == 1282 | // Underlying ASA CloseOut Txn: Wrong CloseTo address (Expected: Smart ASA App Account) 1283 | assert 1284 | load 107 1285 | txnas Assets 1286 | asset_params_get AssetCreator 1287 | store 112 1288 | store 111 1289 | load 112 1290 | bz assetappcloseout_18_l6 1291 | bytec_0 // "smart_asa_id" 1292 | app_global_get 1293 | load 107 1294 | txnas Assets 1295 | == 1296 | // Invalid Smart ASA ID 1297 | assert 1298 | bytec_1 // "frozen" 1299 | app_global_get 1300 | txn Sender 1301 | bytec_1 // "frozen" 1302 | app_local_get 1303 | || 1304 | bnz assetappcloseout_18_l5 1305 | assetappcloseout_18_l2: 1306 | load 108 1307 | txnas Accounts 1308 | global CurrentApplicationAddress 1309 | != 1310 | bnz assetappcloseout_18_l4 1311 | assetappcloseout_18_l3: 1312 | txn Sender 1313 | load 107 1314 | txnas Assets 1315 | asset_holding_get AssetBalance 1316 | store 110 1317 | store 109 1318 | load 107 1319 | txnas Assets 1320 | load 109 1321 | txn Sender 1322 | load 108 1323 | txnas Accounts 1324 | callsub smartasatransferinnertxn_6 1325 | b assetappcloseout_18_l6 1326 | assetappcloseout_18_l4: 1327 | bytec_0 // "smart_asa_id" 1328 | app_global_get 1329 | load 108 1330 | txnas Accounts 1331 | bytec_0 // "smart_asa_id" 1332 | app_local_get 1333 | == 1334 | // Invalid Smart ASA ID 1335 | assert 1336 | b assetappcloseout_18_l3 1337 | assetappcloseout_18_l5: 1338 | load 108 1339 | txnas Accounts 1340 | global CurrentApplicationAddress 1341 | == 1342 | // Wrong CloseTo address: Frozen Smart ASA must be closed-out to creator 1343 | assert 1344 | b assetappcloseout_18_l2 1345 | assetappcloseout_18_l6: 1346 | intc_1 // 1 1347 | return 1348 | 1349 | // asset_destroy 1350 | assetdestroy_19: 1351 | store 113 1352 | bytec_0 // "smart_asa_id" 1353 | app_global_get 1354 | // Smart ASA ID dose not exist 1355 | assert 1356 | bytec_0 // "smart_asa_id" 1357 | app_global_get 1358 | load 113 1359 | txnas Assets 1360 | == 1361 | // Invalid Smart ASA ID 1362 | assert 1363 | txn Sender 1364 | bytec 6 // "manager_addr" 1365 | app_global_get 1366 | == 1367 | // Caller not authorized (must be: Manager Address) 1368 | assert 1369 | load 113 1370 | txnas Assets 1371 | callsub smartasadestroyinnertxn_7 1372 | callsub initglobalstate_0 1373 | retsub 1374 | 1375 | // get_asset_is_frozen 1376 | getassetisfrozen_20: 1377 | txnas Assets 1378 | callsub getterpreconditions_10 1379 | bytec_1 // "frozen" 1380 | app_global_get 1381 | ! 1382 | ! 1383 | retsub 1384 | 1385 | // get_account_is_frozen 1386 | getaccountisfrozen_21: 1387 | store 52 1388 | txnas Assets 1389 | callsub getterpreconditions_10 1390 | load 52 1391 | txnas Accounts 1392 | callsub isvalidaddressbyteslength_8 1393 | load 52 1394 | txnas Accounts 1395 | bytec_1 // "frozen" 1396 | app_local_get 1397 | ! 1398 | ! 1399 | retsub 1400 | 1401 | // get_circulating_supply 1402 | getcirculatingsupply_22: 1403 | store 54 1404 | load 54 1405 | txnas Assets 1406 | callsub getterpreconditions_10 1407 | load 54 1408 | txnas Assets 1409 | callsub circulatingsupply_9 1410 | retsub 1411 | 1412 | // get_optin_min_balance 1413 | getoptinminbalance_23: 1414 | txnas Assets 1415 | callsub getterpreconditions_10 1416 | pushint 157000 // 157000 1417 | retsub 1418 | 1419 | // get_asset_config 1420 | getassetconfig_24: 1421 | txnas Assets 1422 | callsub getterpreconditions_10 1423 | bytec 7 // "total" 1424 | app_global_get 1425 | store 57 1426 | bytec 10 // "decimals" 1427 | app_global_get 1428 | store 58 1429 | load 58 1430 | pushint 4294967296 // 4294967296 1431 | < 1432 | assert 1433 | bytec 8 // "default_frozen" 1434 | app_global_get 1435 | ! 1436 | ! 1437 | store 59 1438 | bytec 11 // "unit_name" 1439 | app_global_get 1440 | store 60 1441 | load 60 1442 | len 1443 | itob 1444 | extract 6 0 1445 | load 60 1446 | concat 1447 | store 60 1448 | bytec 12 // "name" 1449 | app_global_get 1450 | store 61 1451 | load 61 1452 | len 1453 | itob 1454 | extract 6 0 1455 | load 61 1456 | concat 1457 | store 61 1458 | bytec 13 // "url" 1459 | app_global_get 1460 | store 62 1461 | load 62 1462 | len 1463 | itob 1464 | extract 6 0 1465 | load 62 1466 | concat 1467 | store 62 1468 | bytec 14 // "metadata_hash" 1469 | app_global_get 1470 | store 63 1471 | load 63 1472 | len 1473 | itob 1474 | extract 6 0 1475 | load 63 1476 | concat 1477 | store 63 1478 | load 63 1479 | store 64 1480 | bytec 6 // "manager_addr" 1481 | app_global_get 1482 | store 65 1483 | load 65 1484 | len 1485 | pushint 32 // 32 1486 | == 1487 | assert 1488 | bytec_2 // "reserve_addr" 1489 | app_global_get 1490 | store 66 1491 | load 66 1492 | len 1493 | pushint 32 // 32 1494 | == 1495 | assert 1496 | bytec_3 // "freeze_addr" 1497 | app_global_get 1498 | store 67 1499 | load 67 1500 | len 1501 | pushint 32 // 32 1502 | == 1503 | assert 1504 | bytec 4 // "clawback_addr" 1505 | app_global_get 1506 | store 68 1507 | load 68 1508 | len 1509 | pushint 32 // 32 1510 | == 1511 | assert 1512 | load 57 1513 | itob 1514 | load 58 1515 | itob 1516 | extract 4 0 1517 | concat 1518 | bytec 15 // 0x00 1519 | intc_0 // 0 1520 | load 59 1521 | setbit 1522 | concat 1523 | load 60 1524 | store 72 1525 | load 72 1526 | store 71 1527 | pushint 149 // 149 1528 | store 69 1529 | load 69 1530 | load 72 1531 | len 1532 | + 1533 | store 70 1534 | load 70 1535 | intc 4 // 65536 1536 | < 1537 | assert 1538 | load 69 1539 | itob 1540 | extract 6 0 1541 | concat 1542 | load 61 1543 | store 72 1544 | load 71 1545 | load 72 1546 | concat 1547 | store 71 1548 | load 70 1549 | store 69 1550 | load 69 1551 | load 72 1552 | len 1553 | + 1554 | store 70 1555 | load 70 1556 | intc 4 // 65536 1557 | < 1558 | assert 1559 | load 69 1560 | itob 1561 | extract 6 0 1562 | concat 1563 | load 62 1564 | store 72 1565 | load 71 1566 | load 72 1567 | concat 1568 | store 71 1569 | load 70 1570 | store 69 1571 | load 69 1572 | load 72 1573 | len 1574 | + 1575 | store 70 1576 | load 70 1577 | intc 4 // 65536 1578 | < 1579 | assert 1580 | load 69 1581 | itob 1582 | extract 6 0 1583 | concat 1584 | load 64 1585 | store 72 1586 | load 71 1587 | load 72 1588 | concat 1589 | store 71 1590 | load 70 1591 | store 69 1592 | load 69 1593 | itob 1594 | extract 6 0 1595 | concat 1596 | load 65 1597 | concat 1598 | load 66 1599 | concat 1600 | load 67 1601 | concat 1602 | load 68 1603 | concat 1604 | load 71 1605 | concat 1606 | retsub 1607 | -------------------------------------------------------------------------------- /smart_asa_asc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Smart ASA PyTEAL reference implementation based on ARC-20 6 | """ 7 | 8 | __author__ = "Cosimo Bassi, Stefano De Angelis" 9 | __email__ = ", " 10 | 11 | from pyteal import ( 12 | And, 13 | App, 14 | Approve, 15 | Assert, 16 | AssetHolding, 17 | AssetParam, 18 | BareCallActions, 19 | Bytes, 20 | CallConfig, 21 | Concat, 22 | Expr, 23 | Extract, 24 | Global, 25 | Gtxn, 26 | If, 27 | InnerTxn, 28 | InnerTxnBuilder, 29 | Int, 30 | Len, 31 | Mode, 32 | Not, 33 | OnCompleteAction, 34 | OptimizeOptions, 35 | Or, 36 | Reject, 37 | Return, 38 | Router, 39 | Seq, 40 | Subroutine, 41 | Suffix, 42 | TealType, 43 | Txn, 44 | TxnField, 45 | TxnType, 46 | abi, 47 | compileTeal, 48 | ) 49 | from algosdk.future.transaction import StateSchema 50 | from algosdk.constants import key_len_bytes 51 | 52 | 53 | # / --- CONSTANTS 54 | TEAL_VERSION = 7 55 | 56 | # Descriptive field for the binding of Smart ASA App ID into the Underlying ASA url. 57 | SMART_ASA_APP_BINDING = "smart-asa-app-id:" 58 | 59 | # NOTE: The following costs could change over time with protocol upgrades. 60 | OPTIN_COST = 100_000 61 | UINTS_COST = 28_500 62 | BYTES_COST = 50_000 63 | 64 | 65 | def static_attrs(cls): 66 | return [k for k in cls.__dict__ if not k.startswith("__")] 67 | 68 | 69 | # / --- SMART ASA ASC 70 | # / --- --- ERRORS 71 | class Error: 72 | address_length = "Invalid Address length (must be 32 bytes)" 73 | missing_smart_asa_id = "Smart ASA ID does not exist" 74 | invalid_smart_asa_id = "Invalid Smart ASA ID" 75 | not_creator_addr = "Caller not authorized (must be: App Creator Address)" 76 | not_manager_addr = "Caller not authorized (must be: Manager Address)" 77 | not_reserve_addr = "Caller not authorized (must be: Reserve Address)" 78 | not_freeze_addr = "Caller not authorized (must be: Freeze Address)" 79 | not_clawback_addr = "Caller not authorized (must be: Clawback Address)" 80 | asset_frozen = "Smart ASA is frozen" 81 | sender_frozen = "Sender is frozen" 82 | receiver_frozen = "Receiver is frozen" 83 | 84 | 85 | # / --- --- GLOBAL STATE 86 | class GlobalInts: 87 | total = Bytes("total") 88 | decimals = Bytes("decimals") 89 | default_frozen = Bytes("default_frozen") 90 | smart_asa_id = Bytes("smart_asa_id") 91 | frozen = Bytes("frozen") 92 | 93 | 94 | class GlobalBytes: 95 | unit_name = Bytes("unit_name") 96 | name = Bytes("name") 97 | url = Bytes("url") 98 | metadata_hash = Bytes("metadata_hash") 99 | manager_addr = Bytes("manager_addr") 100 | reserve_addr = Bytes("reserve_addr") 101 | freeze_addr = Bytes("freeze_addr") 102 | clawback_addr = Bytes("clawback_addr") 103 | 104 | 105 | class GlobalState(GlobalInts, GlobalBytes): 106 | @staticmethod 107 | def num_uints(): 108 | return len(static_attrs(GlobalInts)) 109 | 110 | @staticmethod 111 | def num_bytes(): 112 | return len(static_attrs(GlobalBytes)) 113 | 114 | @classmethod 115 | def schema(cls): 116 | return StateSchema( 117 | num_uints=cls.num_uints(), 118 | num_byte_slices=cls.num_bytes(), 119 | ) 120 | 121 | 122 | class SmartASAConfig(abi.NamedTuple): 123 | total: abi.Field[abi.Uint64] 124 | decimals: abi.Field[abi.Uint32] 125 | default_frozen: abi.Field[abi.Bool] 126 | unit_name: abi.Field[abi.String] 127 | name: abi.Field[abi.String] 128 | url: abi.Field[abi.String] 129 | metadata_hash: abi.Field[abi.DynamicArray[abi.Byte]] 130 | manager_addr: abi.Field[abi.Address] 131 | reserve_addr: abi.Field[abi.Address] 132 | freeze_addr: abi.Field[abi.Address] 133 | clawback_addr: abi.Field[abi.Address] 134 | 135 | 136 | # / --- --- LOCAL STATE 137 | # NOTE: Local State is needed only if the Smart ASA has `account_frozen`. 138 | # Local State is not needed in case Smart ASA has just "global" `asset_freeze`. 139 | class LocalInts: 140 | smart_asa_id = Bytes("smart_asa_id") 141 | frozen = Bytes("frozen") 142 | 143 | 144 | class LocalBytes: 145 | ... 146 | 147 | 148 | class LocalState(LocalInts, LocalBytes): 149 | @staticmethod 150 | def num_uints(): 151 | return len(static_attrs(LocalInts)) 152 | 153 | @staticmethod 154 | def num_bytes(): 155 | return len(static_attrs(LocalBytes)) 156 | 157 | @classmethod 158 | def schema(cls): 159 | return StateSchema( 160 | num_uints=cls.num_uints(), 161 | num_byte_slices=cls.num_bytes(), 162 | ) 163 | 164 | 165 | # / --- --- SUBROUTINES 166 | @Subroutine(TealType.none) 167 | def init_global_state() -> Expr: 168 | return Seq( 169 | App.globalPut(GlobalState.smart_asa_id, Int(0)), 170 | App.globalPut(GlobalState.total, Int(0)), 171 | App.globalPut(GlobalState.decimals, Int(0)), 172 | App.globalPut(GlobalState.default_frozen, Int(0)), 173 | # NOTE: ASA behaves excluding `unit_name` field if not declared: 174 | App.globalPut(GlobalState.unit_name, Bytes("")), 175 | # NOTE: ASA behaves excluding `name` field if not declared: 176 | App.globalPut(GlobalState.name, Bytes("")), 177 | # NOTE: ASA behaves excluding `url` field if not declared: 178 | App.globalPut(GlobalState.url, Bytes("")), 179 | # NOTE: ASA behaves excluding `metadata_hash` field if not declared: 180 | App.globalPut(GlobalState.metadata_hash, Bytes("")), 181 | App.globalPut(GlobalState.manager_addr, Global.zero_address()), 182 | App.globalPut(GlobalState.reserve_addr, Global.zero_address()), 183 | App.globalPut(GlobalState.freeze_addr, Global.zero_address()), 184 | App.globalPut(GlobalState.clawback_addr, Global.zero_address()), 185 | # Special Smart ASA fields 186 | App.globalPut(GlobalState.frozen, Int(0)), 187 | ) 188 | 189 | 190 | @Subroutine(TealType.none) 191 | def init_local_state() -> Expr: 192 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 193 | return Seq( 194 | App.localPut(Txn.sender(), LocalState.smart_asa_id, smart_asa_id), 195 | App.localPut(Txn.sender(), LocalState.frozen, Int(0)), 196 | ) 197 | 198 | 199 | @Subroutine(TealType.bytes) 200 | def digit_to_ascii(i: Expr) -> Expr: 201 | """digit_to_ascii converts an integer < 10 to the ASCII byte that represents it""" 202 | return Extract(Bytes("0123456789"), i, Int(1)) 203 | 204 | 205 | @Subroutine(TealType.bytes) 206 | def itoa(i: Expr) -> Expr: 207 | """itoa converts an integer to the ASCII byte string it represents.""" 208 | return If( 209 | i == Int(0), 210 | Bytes("0"), 211 | Concat( 212 | If(i / Int(10) > Int(0), itoa(i / Int(10)), Bytes("")), 213 | digit_to_ascii(i % Int(10)), 214 | ), 215 | ) 216 | 217 | 218 | @Subroutine(TealType.bytes) 219 | def strip_len_prefix(abi_encoded: Expr) -> Expr: 220 | return Suffix(abi_encoded, Int(abi.Uint16TypeSpec().byte_length_static())) 221 | 222 | 223 | # / --- --- UNDERLYING ASA CONFIG 224 | UNDERLYING_ASA_TOTAL = Int(2**64 - 1) 225 | UNDERLYING_ASA_DECIMALS = Int(0) 226 | UNDERLYING_ASA_DEFAULT_FROZEN = Int(1) 227 | UNDERLYING_ASA_UNIT_NAME = Bytes("S-ASA") 228 | UNDERLYING_ASA_NAME = Bytes("SMART-ASA") 229 | UNDERLYING_ASA_URL = Concat( 230 | Bytes(SMART_ASA_APP_BINDING), itoa(Global.current_application_id()) 231 | ) 232 | UNDERLYING_ASA_METADATA_HASH = Bytes("") 233 | UNDERLYING_ASA_MANAGER_ADDR = Global.current_application_address() 234 | UNDERLYING_ASA_RESERVE_ADDR = Global.current_application_address() 235 | UNDERLYING_ASA_FREEZE_ADDR = Global.current_application_address() 236 | UNDERLYING_ASA_CLAWBACK_ADDR = Global.current_application_address() 237 | 238 | 239 | @Subroutine(TealType.uint64) 240 | def underlying_asa_create_inner_tx() -> Expr: 241 | return Seq( 242 | InnerTxnBuilder.Execute( 243 | { 244 | TxnField.fee: Int(0), 245 | TxnField.type_enum: TxnType.AssetConfig, 246 | TxnField.config_asset_total: UNDERLYING_ASA_TOTAL, 247 | TxnField.config_asset_decimals: UNDERLYING_ASA_DECIMALS, 248 | TxnField.config_asset_default_frozen: UNDERLYING_ASA_DEFAULT_FROZEN, 249 | TxnField.config_asset_unit_name: UNDERLYING_ASA_UNIT_NAME, 250 | TxnField.config_asset_name: UNDERLYING_ASA_NAME, 251 | TxnField.config_asset_url: UNDERLYING_ASA_URL, 252 | TxnField.config_asset_manager: UNDERLYING_ASA_MANAGER_ADDR, 253 | TxnField.config_asset_reserve: UNDERLYING_ASA_RESERVE_ADDR, 254 | TxnField.config_asset_freeze: UNDERLYING_ASA_FREEZE_ADDR, 255 | TxnField.config_asset_clawback: UNDERLYING_ASA_CLAWBACK_ADDR, 256 | } 257 | ), 258 | Return(InnerTxn.created_asset_id()), 259 | ) 260 | 261 | 262 | @Subroutine(TealType.none) 263 | def smart_asa_transfer_inner_txn( 264 | smart_asa_id: Expr, 265 | asset_amount: Expr, 266 | asset_sender: Expr, 267 | asset_receiver: Expr, 268 | ) -> Expr: 269 | return InnerTxnBuilder.Execute( 270 | { 271 | TxnField.fee: Int(0), 272 | TxnField.type_enum: TxnType.AssetTransfer, 273 | TxnField.xfer_asset: smart_asa_id, 274 | TxnField.asset_amount: asset_amount, 275 | TxnField.asset_sender: asset_sender, 276 | TxnField.asset_receiver: asset_receiver, 277 | } 278 | ) 279 | 280 | 281 | @Subroutine(TealType.none) 282 | def smart_asa_destroy_inner_txn(smart_asa_id: Expr) -> Expr: 283 | return InnerTxnBuilder.Execute( 284 | { 285 | TxnField.fee: Int(0), 286 | TxnField.type_enum: TxnType.AssetConfig, 287 | TxnField.config_asset: smart_asa_id, 288 | } 289 | ) 290 | 291 | 292 | @Subroutine(TealType.none) 293 | def is_valid_address_bytes_length(address: Expr) -> Expr: 294 | # WARNING: Note this check only ensures proper bytes' length on `address`, 295 | # but doesn't ensure that those 32 bytes are a _proper_ Algorand address. 296 | return Assert(Len(address) == Int(key_len_bytes), comment=Error.address_length) 297 | 298 | 299 | @Subroutine(TealType.uint64) 300 | def circulating_supply(asset_id: Expr): 301 | smart_asa_reserve = AssetHolding.balance( 302 | Global.current_application_address(), asset_id 303 | ) 304 | return Seq(smart_asa_reserve, UNDERLYING_ASA_TOTAL - smart_asa_reserve.value()) 305 | 306 | 307 | @Subroutine(TealType.none) 308 | def getter_preconditions(asset_id: Expr) -> Expr: 309 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 310 | is_correct_smart_asa_id = smart_asa_id == asset_id 311 | return Seq( 312 | Assert(smart_asa_id, comment=Error.missing_smart_asa_id), 313 | Assert(is_correct_smart_asa_id, comment=Error.invalid_smart_asa_id), 314 | ) 315 | 316 | 317 | # / --- --- ABI 318 | # / --- --- BARE CALLS 319 | @Subroutine(TealType.none) 320 | def asset_app_create() -> Expr: 321 | return Seq( 322 | # Preconditions 323 | # Not mandatory - Smart ASA Application self validate its state. 324 | Assert( 325 | Txn.global_num_uints() == Int(GlobalState.num_uints()), 326 | comment=f"Wrong State Schema - Expexted Global Ints: " 327 | f"{GlobalState.num_uints()}", 328 | ), 329 | Assert( 330 | Txn.global_num_byte_slices() == Int(GlobalState.num_bytes()), 331 | comment=f"Wrong State Schema - Expexted Global Bytes: " 332 | f"{GlobalState.num_bytes()}", 333 | ), 334 | Assert( 335 | Txn.local_num_uints() == Int(LocalState.num_uints()), 336 | comment=f"Wrong State Schema - Expexted Local Ints: " 337 | f"{LocalState.num_uints()}", 338 | ), 339 | Assert( 340 | Txn.local_num_byte_slices() == Int(LocalState.num_bytes()), 341 | comment=f"Wrong State Schema - Expexted Local Bytes: " 342 | f"{LocalState.num_bytes()}", 343 | ), 344 | init_global_state(), 345 | Approve(), 346 | ) 347 | 348 | 349 | smart_asa_abi = Router( 350 | "Smart ASA ref. implementation", 351 | BareCallActions( 352 | no_op=OnCompleteAction.create_only(asset_app_create()), 353 | # Rules governing a Smart ASA are only in place as long as the 354 | # controlling Smart Contract is not updatable. 355 | update_application=OnCompleteAction.always(Reject()), 356 | # Rules governing a Smart ASA are only in place as long as the 357 | # controlling Smart Contract is not deletable. 358 | delete_application=OnCompleteAction.always(Reject()), 359 | clear_state=OnCompleteAction.call_only(Reject()), 360 | ), 361 | ) 362 | 363 | 364 | # / --- --- METHODS 365 | @smart_asa_abi.method(opt_in=CallConfig.ALL) 366 | def asset_app_optin( 367 | asset: abi.Asset, 368 | underlying_asa_optin: abi.AssetTransferTransaction, 369 | ) -> Expr: 370 | """ 371 | Smart ASA atomic opt-in to Smart ASA App and Underlying ASA. 372 | 373 | Args: 374 | asset: Underlying ASA ID (ref. App Global State: "smart_asa_id"). 375 | underlying_asa_optin: Underlying ASA opt-in transaction. 376 | """ 377 | # On OptIn the frozen status must be set to `True` if account owns any 378 | # units of the underlying ASA. This prevents malicious users to circumvent 379 | # the `default_frozen` status by clearing their Local State. Note that this 380 | # could be avoided by the use of Boxes once available. 381 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 382 | is_correct_smart_asa_id = smart_asa_id == asset.asset_id() 383 | default_frozen = App.globalGet(GlobalState.default_frozen) 384 | freeze_account = App.localPut(Txn.sender(), LocalState.frozen, Int(1)) 385 | account_balance = AssetHolding().balance(Txn.sender(), asset.asset_id()) 386 | optin_to_underlying_asa = account_balance.hasValue() 387 | return Seq( 388 | # Preconditions 389 | Assert(smart_asa_id, comment=Error.missing_smart_asa_id), 390 | Assert(is_correct_smart_asa_id, comment=Error.invalid_smart_asa_id), 391 | Assert( 392 | underlying_asa_optin.get().type_enum() == TxnType.AssetTransfer, 393 | comment="Underlying ASA Opt-In Txn: Wrong Txn Type (Expected: Axfer)", 394 | ), 395 | Assert( 396 | underlying_asa_optin.get().xfer_asset() == smart_asa_id, 397 | comment="Underlying ASA Opt-In Txn: Wrong Asset ID (Expected: Smart ASA ID)", 398 | ), 399 | Assert( 400 | underlying_asa_optin.get().sender() == Txn.sender(), 401 | comment="Underlying ASA Opt-In Txn: Wrong Sender (Expected: App Caller)", 402 | ), 403 | Assert( 404 | underlying_asa_optin.get().asset_receiver() == Txn.sender(), 405 | comment="Underlying ASA Opt-In Txn: Wrong Asset Receiver (Expected: App Caller)", 406 | ), 407 | Assert( 408 | underlying_asa_optin.get().asset_amount() == Int(0), 409 | comment="Underlying ASA Opt-In Txn: Wrong Asset Amount (Expected: 0)", 410 | ), 411 | Assert( 412 | underlying_asa_optin.get().asset_close_to() == Global.zero_address(), 413 | comment="Underlying ASA Opt-In Txn: Wrong Asset CloseTo (Expected: Zero Address)", 414 | ), 415 | account_balance, 416 | Assert(optin_to_underlying_asa, comment="Missing Opt-In to Underlying ASA"), 417 | # Effects 418 | init_local_state(), 419 | If(Or(default_frozen, account_balance.value() > Int(0))).Then(freeze_account), 420 | Approve(), 421 | ) 422 | 423 | 424 | @smart_asa_abi.method 425 | def asset_create( 426 | total: abi.Uint64, 427 | decimals: abi.Uint32, 428 | default_frozen: abi.Bool, 429 | unit_name: abi.String, 430 | name: abi.String, 431 | url: abi.String, 432 | metadata_hash: abi.DynamicArray[abi.Byte], 433 | manager_addr: abi.Address, 434 | reserve_addr: abi.Address, 435 | freeze_addr: abi.Address, 436 | clawback_addr: abi.Address, 437 | *, 438 | output: abi.Uint64, 439 | ) -> Expr: 440 | """ 441 | Create a Smart ASA (triggers inner creation of an Underlying ASA). 442 | 443 | Args: 444 | total: The total number of base units of the Smart ASA to create. 445 | decimals: The number of digits to use after the decimal point when displaying the Smart ASA. If 0, the Smart ASA is not divisible. 446 | default_frozen: Smart ASA default frozen status (True to freeze holdings by default). 447 | unit_name: The name of a unit of Smart ASA. 448 | name: The name of the Smart ASA. 449 | url: Smart ASA external URL. 450 | metadata_hash: Smart ASA metadata hash (suggested 32 bytes hash). 451 | manager_addr: The address of the account that can manage the configuration of the Smart ASA and destroy it. 452 | reserve_addr: The address of the account that holds the reserve (non-minted) units of the asset and can mint or burn units of Smart ASA. 453 | freeze_addr: The address of the account that can freeze/unfreeze holdings of this Smart ASA globally or locally (specific accounts). If empty, freezing is not permitted. 454 | clawback_addr: The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted. 455 | 456 | Returns: 457 | New Smart ASA ID. 458 | """ 459 | 460 | is_creator = Txn.sender() == Global.creator_address() 461 | smart_asa_not_created = Not(App.globalGet(GlobalState.smart_asa_id)) 462 | smart_asa_id = underlying_asa_create_inner_tx() 463 | 464 | return Seq( 465 | # Preconditions 466 | Assert(is_creator, comment=Error.not_creator_addr), 467 | Assert(smart_asa_not_created, comment="Smart ASA ID already exists"), 468 | is_valid_address_bytes_length(manager_addr.get()), 469 | is_valid_address_bytes_length(reserve_addr.get()), 470 | is_valid_address_bytes_length(freeze_addr.get()), 471 | is_valid_address_bytes_length(clawback_addr.get()), 472 | # Effects 473 | # Underlying ASA creation 474 | App.globalPut(GlobalState.smart_asa_id, smart_asa_id), 475 | # Smart ASA properties 476 | App.globalPut(GlobalState.total, total.get()), 477 | App.globalPut(GlobalState.decimals, decimals.get()), 478 | App.globalPut(GlobalState.default_frozen, default_frozen.get()), 479 | App.globalPut(GlobalState.unit_name, unit_name.get()), 480 | App.globalPut(GlobalState.name, name.get()), 481 | App.globalPut(GlobalState.url, url.get()), 482 | App.globalPut( 483 | GlobalState.metadata_hash, strip_len_prefix(metadata_hash.encode()) 484 | ), 485 | App.globalPut(GlobalState.manager_addr, manager_addr.get()), 486 | App.globalPut(GlobalState.reserve_addr, reserve_addr.get()), 487 | App.globalPut(GlobalState.freeze_addr, freeze_addr.get()), 488 | App.globalPut(GlobalState.clawback_addr, clawback_addr.get()), 489 | output.set(App.globalGet(GlobalState.smart_asa_id)), 490 | ) 491 | 492 | 493 | @smart_asa_abi.method 494 | def asset_config( 495 | config_asset: abi.Asset, 496 | total: abi.Uint64, 497 | decimals: abi.Uint32, 498 | default_frozen: abi.Bool, 499 | unit_name: abi.String, 500 | name: abi.String, 501 | url: abi.String, 502 | metadata_hash: abi.DynamicArray[abi.Byte], 503 | manager_addr: abi.Address, 504 | reserve_addr: abi.Address, 505 | freeze_addr: abi.Address, 506 | clawback_addr: abi.Address, 507 | ) -> Expr: 508 | """ 509 | Configure the Smart ASA. Use existing values for unchanged parameters. Setting Smart ASA roles to zero-address is irreversible. 510 | 511 | Args: 512 | config_asset: Underlying ASA ID to configure (ref. App Global State: "smart_asa_id"). 513 | total: The total number of base units of the Smart ASA to create. It can not be configured to less than its current circulating supply. 514 | decimals: The number of digits to use after the decimal point when displaying the Smart ASA. If 0, the Smart ASA is not divisible. 515 | default_frozen: Smart ASA default frozen status (True to freeze holdings by default). 516 | unit_name: The name of a unit of Smart ASA. 517 | name: The name of the Smart ASA. 518 | url: Smart ASA external URL. 519 | metadata_hash: Smart ASA metadata hash (suggested 32 bytes hash). 520 | manager_addr: The address of the account that can manage the configuration of the Smart ASA and destroy it. 521 | reserve_addr: The address of the account that holds the reserve (non-minted) units of the asset and can mint or burn units of Smart ASA. 522 | freeze_addr: The address of the account that can freeze/unfreeze holdings of this Smart ASA globally or locally (specific accounts). If empty, freezing is not permitted. 523 | clawback_addr: The address of the account that can clawback holdings of this asset. If empty, clawback is not permitted. 524 | """ 525 | 526 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 527 | current_manager_addr = App.globalGet(GlobalState.manager_addr) 528 | current_reserve_addr = App.globalGet(GlobalState.reserve_addr) 529 | current_freeze_addr = App.globalGet(GlobalState.freeze_addr) 530 | current_clawback_addr = App.globalGet(GlobalState.clawback_addr) 531 | 532 | is_manager_addr = Txn.sender() == current_manager_addr 533 | is_correct_smart_asa_id = smart_asa_id == config_asset.asset_id() 534 | 535 | update_reserve_addr = current_reserve_addr != reserve_addr.get() 536 | update_freeze_addr = current_freeze_addr != freeze_addr.get() 537 | update_clawback_addr = current_clawback_addr != clawback_addr.get() 538 | 539 | # NOTE: In ref. implementation Smart ASA total can not be configured to 540 | # less than its current circulating supply. 541 | is_valid_total = total.get() >= circulating_supply(smart_asa_id) 542 | 543 | return Seq( 544 | # Preconditions 545 | Assert(smart_asa_id, comment=Error.missing_smart_asa_id), 546 | # NOTE: useless in ref. impl since 1 ASA : 1 App 547 | Assert(is_correct_smart_asa_id, comment=Error.invalid_smart_asa_id), 548 | is_valid_address_bytes_length(manager_addr.get()), 549 | is_valid_address_bytes_length(reserve_addr.get()), 550 | is_valid_address_bytes_length(freeze_addr.get()), 551 | is_valid_address_bytes_length(clawback_addr.get()), 552 | Assert(is_manager_addr, comment=Error.not_manager_addr), 553 | If(update_reserve_addr).Then( 554 | Assert( 555 | current_reserve_addr != Global.zero_address(), 556 | comment="Reserve Address has been deleted", 557 | ) 558 | ), 559 | If(update_freeze_addr).Then( 560 | Assert( 561 | current_freeze_addr != Global.zero_address(), 562 | comment="Freeze Address has been deleted", 563 | ) 564 | ), 565 | If(update_clawback_addr).Then( 566 | Assert( 567 | current_clawback_addr != Global.zero_address(), 568 | comment="Clawback Address has been deleted", 569 | ) 570 | ), 571 | Assert(is_valid_total, comment="Invalid Total (must be >= Circulating Supply)"), 572 | # Effects 573 | App.globalPut(GlobalState.total, total.get()), 574 | App.globalPut(GlobalState.decimals, decimals.get()), 575 | App.globalPut(GlobalState.default_frozen, default_frozen.get()), 576 | App.globalPut(GlobalState.unit_name, unit_name.get()), 577 | App.globalPut(GlobalState.name, name.get()), 578 | App.globalPut(GlobalState.url, url.get()), 579 | App.globalPut( 580 | GlobalState.metadata_hash, strip_len_prefix(metadata_hash.encode()) 581 | ), 582 | App.globalPut(GlobalState.manager_addr, manager_addr.get()), 583 | App.globalPut(GlobalState.reserve_addr, reserve_addr.get()), 584 | App.globalPut(GlobalState.freeze_addr, freeze_addr.get()), 585 | App.globalPut(GlobalState.clawback_addr, clawback_addr.get()), 586 | ) 587 | 588 | 589 | @smart_asa_abi.method 590 | def asset_transfer( 591 | xfer_asset: abi.Asset, 592 | asset_amount: abi.Uint64, 593 | asset_sender: abi.Account, 594 | asset_receiver: abi.Account, 595 | ) -> Expr: 596 | """ 597 | Smart ASA transfers: regular, clawback (Clawback Address), mint or burn (Reserve Address). 598 | 599 | Args: 600 | xfer_asset: Underlying ASA ID to transfer (ref. App Global State: "smart_asa_id"). 601 | asset_amount: Smart ASA amount to transfer. 602 | asset_sender: Smart ASA sender, for regular transfer this must be equal to the Smart ASA App caller. 603 | asset_receiver: The recipient of the Smart ASA transfer. 604 | """ 605 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 606 | clawback_addr = App.globalGet(GlobalState.clawback_addr) 607 | is_not_clawback = And( 608 | Txn.sender() == asset_sender.address(), 609 | Txn.sender() != clawback_addr, 610 | ) 611 | 612 | # NOTE: Ref. implementation grants _minting_ premission to `reserve_addr`, 613 | # has restriction no restriction on who is the minting _receiver_. 614 | # WARNING: Setting Smart ASA `reserve` to ZERO_ADDRESS switchs-off minting. 615 | is_minting = And( 616 | Txn.sender() == App.globalGet(GlobalState.reserve_addr), 617 | asset_sender.address() == Global.current_application_address(), 618 | ) 619 | 620 | # NOTE: Ref. implementation grants _burning_ premission to `reserve_addr`, 621 | # has restriction both on burning _sender_ and _receiver_ to prevent 622 | # _clawback_ throug burning. 623 | # WARNING: Setting Smart ASA `reserve` to ZERO_ADDRESS switchs-off burning. 624 | is_burning = And( 625 | Txn.sender() == App.globalGet(GlobalState.reserve_addr), 626 | asset_sender.address() == App.globalGet(GlobalState.reserve_addr), 627 | asset_receiver.address() == Global.current_application_address(), 628 | ) 629 | 630 | is_clawback = Txn.sender() == clawback_addr 631 | is_correct_smart_asa_id = smart_asa_id == xfer_asset.asset_id() 632 | 633 | # NOTE: Ref. implementation checks that `smart_asa_id` is correct in Local 634 | # State since the App could generate a new Smart ASA (if the previous one 635 | # has been dystroied) requiring users to opt-in again to gain a coherent 636 | # new `frozen` status. 637 | is_current_smart_asa_id = And( 638 | smart_asa_id == App.localGet(asset_sender.address(), LocalState.smart_asa_id), 639 | smart_asa_id == App.localGet(asset_receiver.address(), LocalState.smart_asa_id), 640 | ) 641 | asset_frozen = App.globalGet(GlobalState.frozen) 642 | asset_sender_frozen = App.localGet(asset_sender.address(), LocalState.frozen) 643 | asset_receiver_frozen = App.localGet(asset_receiver.address(), LocalState.frozen) 644 | return Seq( 645 | # Preconditions 646 | Assert(smart_asa_id, comment=Error.missing_smart_asa_id), 647 | Assert(is_correct_smart_asa_id, comment=Error.invalid_smart_asa_id), 648 | is_valid_address_bytes_length(asset_sender.address()), 649 | is_valid_address_bytes_length(asset_receiver.address()), 650 | If(is_not_clawback) 651 | .Then( 652 | # Asset Regular Transfer Preconditions 653 | Assert(Not(asset_frozen), comment=Error.asset_frozen), 654 | Assert(Not(asset_sender_frozen), comment=Error.sender_frozen), 655 | Assert(Not(asset_receiver_frozen), comment=Error.receiver_frozen), 656 | Assert(is_current_smart_asa_id, comment=Error.invalid_smart_asa_id), 657 | ) 658 | .ElseIf(is_minting) 659 | .Then( 660 | # Asset Minting Preconditions 661 | Assert(Not(asset_frozen), comment=Error.asset_frozen), 662 | Assert(Not(asset_receiver_frozen), comment=Error.receiver_frozen), 663 | Assert( 664 | smart_asa_id 665 | == App.localGet(asset_receiver.address(), LocalState.smart_asa_id), 666 | comment=Error.invalid_smart_asa_id, 667 | ), 668 | # NOTE: Ref. implementation prevents minting more than `total`. 669 | Assert( 670 | circulating_supply(smart_asa_id) + asset_amount.get() 671 | <= App.globalGet(GlobalState.total), 672 | comment="Over-minting (can not mint more than Total)", 673 | ), 674 | ) 675 | .ElseIf(is_burning) 676 | .Then( 677 | # Asset Burning Preconditions 678 | Assert(Not(asset_frozen), comment=Error.asset_frozen), 679 | Assert(Not(asset_sender_frozen), comment=Error.sender_frozen), 680 | Assert( 681 | smart_asa_id 682 | == App.localGet(asset_sender.address(), LocalState.smart_asa_id), 683 | comment=Error.invalid_smart_asa_id, 684 | ), 685 | ) 686 | .Else( 687 | # Asset Clawback Preconditions 688 | Assert(is_clawback, comment=Error.not_clawback_addr), 689 | # NOTE: `is_current_smart_asa_id` implicitly checks that both 690 | # `asset_sender` and `asset_receiver` opted-in the Smart ASA 691 | # App. This ensures that _mint_ and _burn_ can not be 692 | # executed as _clawback_, since the Smart ASA App can not 693 | # opt-in to itself. 694 | Assert(is_current_smart_asa_id, comment=Error.invalid_smart_asa_id), 695 | ), 696 | # Effects 697 | smart_asa_transfer_inner_txn( 698 | xfer_asset.asset_id(), 699 | asset_amount.get(), 700 | asset_sender.address(), 701 | asset_receiver.address(), 702 | ), 703 | ) 704 | 705 | 706 | @smart_asa_abi.method 707 | def asset_freeze(freeze_asset: abi.Asset, asset_frozen: abi.Bool) -> Expr: 708 | """ 709 | Smart ASA global freeze (all accounts), called by the Freeze Address. 710 | 711 | Args: 712 | freeze_asset: Underlying ASA ID to freeze/unfreeze (ref. App Global State: "smart_asa_id"). 713 | asset_frozen: Smart ASA ID forzen status. 714 | """ 715 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 716 | is_correct_smart_asa_id = smart_asa_id == freeze_asset.asset_id() 717 | is_freeze_addr = Txn.sender() == App.globalGet(GlobalState.freeze_addr) 718 | return Seq( 719 | # Asset Freeze Preconditions 720 | Assert( 721 | smart_asa_id, 722 | comment=Error.missing_smart_asa_id, 723 | ), 724 | Assert( 725 | is_correct_smart_asa_id, 726 | comment=Error.invalid_smart_asa_id, 727 | ), 728 | Assert( 729 | is_freeze_addr, 730 | comment=Error.not_freeze_addr, 731 | ), 732 | # Effects 733 | App.globalPut(GlobalState.frozen, asset_frozen.get()), 734 | ) 735 | 736 | 737 | @smart_asa_abi.method 738 | def account_freeze( 739 | freeze_asset: abi.Asset, 740 | freeze_account: abi.Account, 741 | asset_frozen: abi.Bool, 742 | ) -> Expr: 743 | """ 744 | Smart ASA local freeze (account specific), called by the Freeze Address. 745 | 746 | Args: 747 | freeze_asset: Underlying ASA ID to freeze/unfreeze (ref. App Global State: "smart_asa_id"). 748 | freeze_account: Account to freeze/unfreeze. 749 | asset_frozen: Smart ASA ID forzen status. 750 | """ 751 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 752 | is_correct_smart_asa_id = smart_asa_id == freeze_asset.asset_id() 753 | is_freeze_addr = Txn.sender() == App.globalGet(GlobalState.freeze_addr) 754 | return Seq( 755 | # Account Freeze Preconditions 756 | is_valid_address_bytes_length(freeze_account.address()), 757 | Assert( 758 | smart_asa_id, 759 | comment=Error.missing_smart_asa_id, 760 | ), 761 | Assert( 762 | is_correct_smart_asa_id, 763 | comment=Error.invalid_smart_asa_id, 764 | ), 765 | Assert( 766 | is_freeze_addr, 767 | comment=Error.not_freeze_addr, 768 | ), 769 | # Effects 770 | App.localPut(freeze_account.address(), LocalState.frozen, asset_frozen.get()), 771 | ) 772 | 773 | 774 | @smart_asa_abi.method(close_out=CallConfig.ALL) 775 | def asset_app_closeout( 776 | close_asset: abi.Asset, 777 | close_to: abi.Account, 778 | ) -> Expr: 779 | """ 780 | Smart ASA atomic close-out of Smart ASA App and Underlying ASA. 781 | 782 | Args: 783 | close_asset: Underlying ASA ID to close-out (ref. App Global State: "smart_asa_id"). 784 | close_to: Account to send all Smart ASA reminder to. If the asset/account is forzen then this must be set to Smart ASA Creator. 785 | """ 786 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 787 | is_correct_smart_asa_id = smart_asa_id == close_asset.asset_id() 788 | current_smart_asa_id = App.localGet(Txn.sender(), LocalState.smart_asa_id) 789 | is_current_smart_asa_id = current_smart_asa_id == close_asset.asset_id() 790 | account_balance = AssetHolding().balance(Txn.sender(), close_asset.asset_id()) 791 | asset_creator = AssetParam().creator(close_asset.asset_id()) 792 | asset_frozen = App.globalGet(GlobalState.frozen) 793 | asset_closer_frozen = App.localGet(Txn.sender(), LocalState.frozen) 794 | asa_closeout_relative_idx = Txn.group_index() + Int(1) 795 | return Seq( 796 | # Preconditions 797 | # NOTE: Smart ASA existence is not checked by default on close-out 798 | # since would be impossible to close-out destroyed assets. 799 | is_valid_address_bytes_length(close_to.address()), 800 | Assert( 801 | is_current_smart_asa_id, 802 | comment=Error.invalid_smart_asa_id, 803 | ), 804 | Assert( 805 | Global.group_size() > asa_closeout_relative_idx, 806 | comment="Smart ASA CloseOut: Wrong group size (Expected: 2)", 807 | ), 808 | Assert( 809 | Gtxn[asa_closeout_relative_idx].type_enum() == TxnType.AssetTransfer, 810 | comment="Underlying ASA CloseOut Txn: Wrong Txn type (Expected: Axfer)", 811 | ), 812 | Assert( 813 | Gtxn[asa_closeout_relative_idx].xfer_asset() == close_asset.asset_id(), 814 | comment="Underlying ASA CloseOut Txn: Wrong ASA ID (Expected: Smart ASA ID)", 815 | ), 816 | Assert( 817 | Gtxn[asa_closeout_relative_idx].sender() == Txn.sender(), 818 | comment="Underlying ASA CloseOut Txn: Wrong sender (Expected: Smart ASA CloseOut caller)", 819 | ), 820 | Assert( 821 | Gtxn[asa_closeout_relative_idx].asset_amount() == Int(0), 822 | comment="Underlying ASA CloseOut Txn: Wrong amount (Expected: 0)", 823 | ), 824 | Assert( 825 | Gtxn[asa_closeout_relative_idx].asset_close_to() 826 | == Global.current_application_address(), 827 | comment="Underlying ASA CloseOut Txn: Wrong CloseTo address (Expected: Smart ASA App Account)", 828 | ), 829 | # Effects 830 | asset_creator, 831 | # NOTE: Skip checks if Underlying ASA has been destroyed to avoid 832 | # users' lock-in. 833 | If(asset_creator.hasValue()).Then( 834 | # NOTE: Smart ASA has not been destroyed. 835 | Assert(is_correct_smart_asa_id, comment=Error.invalid_smart_asa_id), 836 | If(Or(asset_frozen, asset_closer_frozen)).Then( 837 | # NOTE: If Smart ASA is frozen, users can only close-out to 838 | # Creator 839 | Assert( 840 | close_to.address() == Global.current_application_address(), 841 | comment="Wrong CloseTo address: Frozen Smart ASA must be closed-out to creator", 842 | ), 843 | ), 844 | If(close_to.address() != Global.current_application_address()).Then( 845 | # NOTE: If the target of close-out is not Creator, it MUST be 846 | # opted-in to the current Smart ASA. 847 | Assert( 848 | smart_asa_id 849 | == App.localGet(close_to.address(), LocalState.smart_asa_id), 850 | comment=Error.invalid_smart_asa_id, 851 | ) 852 | ), 853 | account_balance, 854 | smart_asa_transfer_inner_txn( 855 | close_asset.asset_id(), 856 | account_balance.value(), 857 | Txn.sender(), 858 | close_to.address(), 859 | ), 860 | ), 861 | # NOTE: If Smart ASA has been destroyed: 862 | # 1. The close-to address could be anyone 863 | # 2. No InnerTxn happens 864 | Approve(), 865 | ) 866 | 867 | 868 | @smart_asa_abi.method 869 | def asset_destroy(destroy_asset: abi.Asset) -> Expr: 870 | """ 871 | Destroy the Underlying ASA, must be called by Manager Address. 872 | 873 | Args: 874 | destroy_asset: Underlying ASA ID to destroy (ref. App Global State: "smart_asa_id"). 875 | """ 876 | smart_asa_id = App.globalGet(GlobalState.smart_asa_id) 877 | is_correct_smart_asa_id = smart_asa_id == destroy_asset.asset_id() 878 | is_manager_addr = Txn.sender() == App.globalGet(GlobalState.manager_addr) 879 | return Seq( 880 | # Asset Destroy Preconditions 881 | Assert( 882 | smart_asa_id, 883 | comment=Error.missing_smart_asa_id, 884 | ), 885 | Assert( 886 | is_correct_smart_asa_id, 887 | comment=Error.invalid_smart_asa_id, 888 | ), 889 | Assert( 890 | is_manager_addr, 891 | comment=Error.not_manager_addr, 892 | ), 893 | # Effects 894 | smart_asa_destroy_inner_txn(destroy_asset.asset_id()), 895 | init_global_state(), 896 | ) 897 | 898 | 899 | # / --- --- GETTERS 900 | @smart_asa_abi.method 901 | def get_asset_is_frozen(freeze_asset: abi.Asset, *, output: abi.Bool) -> Expr: 902 | """ 903 | Get Smart ASA global frozen status. 904 | 905 | Args: 906 | freeze_asset: Underlying ASA ID (ref. App Global State: "smart_asa_id"). 907 | 908 | Returns: 909 | Smart ASA global frozen status. 910 | """ 911 | return Seq( 912 | # Preconditions 913 | getter_preconditions(freeze_asset.asset_id()), 914 | # Effects 915 | output.set(App.globalGet(GlobalState.frozen)), 916 | ) 917 | 918 | 919 | @smart_asa_abi.method 920 | def get_account_is_frozen( 921 | freeze_asset: abi.Asset, freeze_account: abi.Account, *, output: abi.Bool 922 | ) -> Expr: 923 | """ 924 | Get Smart ASA local frozen status (account specific). 925 | 926 | Args: 927 | freeze_asset: Underlying ASA ID (ref. App Global State: "smart_asa_id"). 928 | freeze_account: Account to check. 929 | 930 | Returns: 931 | Smart ASA local frozen status (account specific). 932 | """ 933 | return Seq( 934 | # Preconditions 935 | getter_preconditions(freeze_asset.asset_id()), 936 | is_valid_address_bytes_length(freeze_account.address()), 937 | # Effects 938 | output.set(App.localGet(freeze_account.address(), LocalState.frozen)), 939 | ) 940 | 941 | 942 | @smart_asa_abi.method 943 | def get_circulating_supply(asset: abi.Asset, *, output: abi.Uint64) -> Expr: 944 | """ 945 | Get Smart ASA circulating supply. 946 | 947 | Args: 948 | asset: Underlying ASA ID (ref. App Global State: "smart_asa_id"). 949 | 950 | Returns: 951 | Smart ASA circulating supply. 952 | """ 953 | return Seq( 954 | # Preconditions 955 | getter_preconditions(asset.asset_id()), 956 | # Effects 957 | output.set(circulating_supply(asset.asset_id())), 958 | ) 959 | 960 | 961 | @smart_asa_abi.method 962 | def get_optin_min_balance(asset: abi.Asset, *, output: abi.Uint64) -> Expr: 963 | """ 964 | Get Smart ASA required minimum balance (including Underlying ASA and App Local State). 965 | 966 | Args: 967 | asset: Underlying ASA ID (ref. App Global State: "smart_asa_id"). 968 | 969 | Returns: 970 | Smart ASA required minimum balance in microALGO. 971 | """ 972 | min_balance = Int( 973 | OPTIN_COST 974 | + UINTS_COST * LocalState.num_uints() 975 | + BYTES_COST * LocalState.num_bytes() 976 | ) 977 | 978 | return Seq( 979 | # Preconditions 980 | getter_preconditions(asset.asset_id()), 981 | # Effects 982 | output.set(min_balance), 983 | ) 984 | 985 | 986 | @smart_asa_abi.method 987 | def get_asset_config(asset: abi.Asset, *, output: SmartASAConfig) -> Expr: 988 | """ 989 | Get Smart ASA configuration. 990 | 991 | Args: 992 | asset: Underlying ASA ID (ref. App Global State: "smart_asa_id"). 993 | 994 | Returns: 995 | Smart ASA configuration parameters. 996 | """ 997 | return Seq( 998 | # Preconditions 999 | getter_preconditions(asset.asset_id()), 1000 | # Effects 1001 | (total := abi.Uint64()).set(App.globalGet(GlobalState.total)), 1002 | (decimals := abi.Uint32()).set(App.globalGet(GlobalState.decimals)), 1003 | (default_frozen := abi.Bool()).set(App.globalGet(GlobalState.default_frozen)), 1004 | (unit_name := abi.String()).set(App.globalGet(GlobalState.unit_name)), 1005 | (name := abi.String()).set(App.globalGet(GlobalState.name)), 1006 | (url := abi.String()).set(App.globalGet(GlobalState.url)), 1007 | (metadata_hash_str := abi.String()).set( 1008 | App.globalGet(GlobalState.metadata_hash) 1009 | ), 1010 | (metadata_hash := abi.make(abi.DynamicArray[abi.Byte])).decode( 1011 | metadata_hash_str.encode() 1012 | ), 1013 | (manager_addr := abi.Address()).set(App.globalGet(GlobalState.manager_addr)), 1014 | (reserve_addr := abi.Address()).set(App.globalGet(GlobalState.reserve_addr)), 1015 | (freeze_addr := abi.Address()).set(App.globalGet(GlobalState.freeze_addr)), 1016 | (clawback_addr := abi.Address()).set(App.globalGet(GlobalState.clawback_addr)), 1017 | output.set( 1018 | total, 1019 | decimals, 1020 | default_frozen, 1021 | unit_name, 1022 | name, 1023 | url, 1024 | metadata_hash, 1025 | manager_addr, 1026 | reserve_addr, 1027 | freeze_addr, 1028 | clawback_addr, 1029 | ), 1030 | ) 1031 | 1032 | 1033 | def compile_stateful(program: Expr) -> str: 1034 | return compileTeal( 1035 | program, 1036 | Mode.Application, 1037 | version=TEAL_VERSION, 1038 | assembleConstants=True, 1039 | optimize=OptimizeOptions(scratch_slots=True), 1040 | ) 1041 | 1042 | 1043 | if __name__ == "__main__": 1044 | # Allow quickly testing compilation. 1045 | from smart_asa_test import test_compile 1046 | 1047 | test_compile(*smart_asa_abi.build_program()) 1048 | -------------------------------------------------------------------------------- /smart_asa_clear.teal: -------------------------------------------------------------------------------- 1 | #pragma version 7 2 | intcblock 0 3 | txn NumAppArgs 4 | intc_0 // 0 5 | == 6 | bnz main_l2 7 | err 8 | main_l2: 9 | intc_0 // 0 10 | return 11 | -------------------------------------------------------------------------------- /smart_asa_cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Smart ASA (ARC-20 reference implementation) 3 | 4 | Usage: 5 | smart_asa create [--decimals=] [--default-frozen=] 6 | [--name=] [--unit-name=] [--metadata-hash=] 7 | [--url=] [--manager=] [--reserve=] 8 | [--freeze=] [--clawback=] 9 | smart_asa config [--new-total=] [--new-decimals=] 10 | [--new-default-frozen=] [--new-name=] 11 | [--new-unit-name=] [--new-metadata-hash=] 12 | [--new-url=] [--new-manager=] [--new-reserve=] 13 | [--new-freeze=] [--new-clawback=] 14 | smart_asa destroy 15 | smart_asa freeze (--asset | --account=) 16 | smart_asa optin 17 | smart_asa optout 18 | smart_asa send 19 | [--reserve= | --clawback=] 20 | smart_asa info [--account=] 21 | smart_asa get [--account=] 22 | smart_asa [--help] 23 | 24 | Commands: 25 | create Create a Smart ASA 26 | config Configure a Smart ASA 27 | destroy Destroy a Smart ASA 28 | freeze Freeze whole Smart ASA or specific account, = 1 is forzen 29 | optin Optin Smart ASAs 30 | optout Optout Smart ASAs 31 | send Transfer Smart ASAs 32 | info Look up current parameters for Smart ASA or specific account 33 | get Look up a parameter for Smart ASA 34 | 35 | Options: 36 | -h, --help 37 | -d, --decimals= [default: 0] 38 | -z, --default-frozen= [default: 0] 39 | -n, --name= [default: ] 40 | -u, --unit-name= [default: ] 41 | -l, --url= [default: ] 42 | -s, --metadata-hash= [default: ] 43 | -m, --manager= Default to Smart ASA Creator 44 | -r, --reserve= Default to Smart ASA Creator 45 | -f, --freeze= Default to Smart ASA Creator 46 | -c, --clawback= Default to Smart ASA Creator 47 | """ 48 | 49 | import sys 50 | from docopt import docopt 51 | 52 | from algosdk.abi import Contract 53 | 54 | from account import Account, AppAccount 55 | from sandbox import Sandbox 56 | from smart_asa_asc import ( 57 | compile_stateful, 58 | smart_asa_abi, 59 | ) 60 | from smart_asa_client import ( 61 | get_smart_asa_params, 62 | smart_asa_account_freeze, 63 | smart_asa_closeout, 64 | smart_asa_app_create, 65 | smart_asa_optin, 66 | smart_asa_create, 67 | smart_asa_config, 68 | smart_asa_destroy, 69 | smart_asa_freeze, 70 | smart_asa_get, 71 | smart_asa_transfer, 72 | ) 73 | 74 | 75 | def smart_asa_info(smart_asa_id: int) -> None: 76 | smart_asa = get_smart_asa_params(Sandbox.algod_client, smart_asa_id) 77 | print( 78 | f""" 79 | Asset ID: {smart_asa['smart_asa_id']} 80 | App ID: {smart_asa['app_id']} 81 | App Address: {smart_asa['app_address']} 82 | Creator: {smart_asa['creator_addr']} 83 | Asset name: {smart_asa['name']} 84 | 85 | Unit name: {smart_asa['unit_name']} 86 | 87 | Maximum issue: {smart_asa['total']} {smart_asa['unit_name']} 88 | Issued: {smart_asa['circulating_supply']} {smart_asa['unit_name']} 89 | Decimals: {smart_asa['decimals']} 90 | Global frozen: {smart_asa['frozen']} 91 | Default frozen: {smart_asa['default_frozen']} 92 | Manager address: {smart_asa['manager_addr']} 93 | Reserve address: {smart_asa['reserve_addr']} 94 | Freeze address: {smart_asa['freeze_addr']} 95 | Clawback address: {smart_asa['clawback_addr']} 96 | """ 97 | ) 98 | 99 | 100 | def args_types(args: dict) -> dict: 101 | if args[""] is not None: 102 | args[""] = int(args[""]) 103 | 104 | args["--decimals"] = int(args["--decimals"]) 105 | 106 | args["--default-frozen"] = int(args["--default-frozen"]) 107 | assert args["--default-frozen"] == 0 or args["--default-frozen"] == 1 108 | args["--default-frozen"] = bool(args["--default-frozen"]) 109 | 110 | if args[""] is not None: 111 | args[""] = int(args[""]) 112 | 113 | if args["--new-total"] is not None: 114 | args["--new-total"] = int(args["--new-total"]) 115 | 116 | if args["--new-decimals"] is not None: 117 | args["--new-decimals"] = int(args["--new-decimals"]) 118 | 119 | if args["--new-default-frozen"] is not None: 120 | args["--new-default-frozen"] = int(args["--new-default-frozen"]) 121 | assert args["--new-default-frozen"] == 0 or args["--new-default-frozen"] == 1 122 | args["--new-default-frozen"] = bool(args["--new-default-frozen"]) 123 | 124 | if args[""] is not None: 125 | args[""] = int(args[""]) 126 | assert args[""] == 0 or args[""] == 1 127 | args[""] = bool(args[""]) 128 | 129 | if args[""] is not None: 130 | args[""] = int(args[""]) 131 | 132 | return args 133 | 134 | 135 | def asset_create( 136 | args: dict, 137 | approval: str, 138 | clear: str, 139 | contract: Contract, 140 | ) -> None: 141 | creator = Sandbox.from_public_key(args[""]) 142 | 143 | print("\n --- Creating Smart ASA App...") 144 | smart_asa_app = smart_asa_app_create(approval, clear, creator) 145 | print(" --- Smart ASA App ID:", smart_asa_app.app_id) 146 | 147 | print("\n --- Funding Smart ASA App with 1 ALGO...") 148 | creator.pay(receiver=smart_asa_app, amount=1_000_000) 149 | 150 | print("\n --- Creating Smart ASA...") 151 | smart_asa_id = smart_asa_create( 152 | smart_asa_contract=contract, 153 | smart_asa_app=smart_asa_app, 154 | creator=creator, 155 | total=args[""], 156 | decimals=args["--decimals"], 157 | default_frozen=args["--default-frozen"], 158 | name=args["--name"], 159 | unit_name=args["--unit-name"], 160 | url=args["--url"], 161 | metadata_hash=args["--metadata-hash"], 162 | manager_addr=args["--manager"], 163 | reserve_addr=args["--reserve"], 164 | freeze_addr=args["--freeze"], 165 | clawback_addr=args["--clawback"], 166 | ) 167 | return print(" --- Created Smart ASA with ID:", smart_asa_id, "\n") 168 | 169 | 170 | def asset_config( 171 | args: dict, 172 | contract: Contract, 173 | smart_asa_app: AppAccount, 174 | ) -> None: 175 | manager = Sandbox.from_public_key(args[""]) 176 | 177 | print(f"\n --- Configuring Smart ASA {args['']}...") 178 | smart_asa_config( 179 | smart_asa_contract=contract, 180 | smart_asa_app=smart_asa_app, 181 | manager=manager, 182 | asset_id=args[""], 183 | config_total=args["--new-total"], 184 | config_decimals=args["--new-decimals"], 185 | config_default_frozen=args["--new-default-frozen"], 186 | config_name=args["--new-name"], 187 | config_unit_name=args["--new-unit-name"], 188 | config_url=args["--new-url"], 189 | config_metadata_hash=args["--new-metadata-hash"], 190 | config_manager_addr=args["--new-manager"], 191 | config_reserve_addr=args["--new-reserve"], 192 | config_freeze_addr=args["--new-freeze"], 193 | config_clawback_addr=args["--new-clawback"], 194 | ) 195 | return print(f" --- Smart ASA {args['']} configured!\n") 196 | 197 | 198 | def asset_destroy( 199 | args: dict, 200 | contract: Contract, 201 | smart_asa_app: AppAccount, 202 | ) -> None: 203 | manager = Sandbox.from_public_key(args[""]) 204 | 205 | print(f"\n --- Destroying Smart ASA {args['']}...") 206 | smart_asa_destroy( 207 | smart_asa_contract=contract, 208 | smart_asa_app=smart_asa_app, 209 | manager=manager, 210 | destroy_asset=args[""], 211 | ) 212 | return print(f" --- Smart ASA {args['']} destroyed!\n") 213 | 214 | 215 | def asset_or_account_freeze( 216 | args: dict, 217 | contract: Contract, 218 | smart_asa_app: AppAccount, 219 | ) -> None: 220 | freezer = Sandbox.from_public_key(args[""]) 221 | 222 | if args[""]: 223 | action = "Freezing" 224 | else: 225 | action = "Unfreezing" 226 | 227 | if args["--asset"]: 228 | print(f"\n --- {action} Smart ASA {args['']}...\n") 229 | return smart_asa_freeze( 230 | smart_asa_contract=contract, 231 | smart_asa_app=smart_asa_app, 232 | freezer=freezer, 233 | freeze_asset=args[""], 234 | asset_frozen=args[""], 235 | ) 236 | else: 237 | print(f"\n --- {action} account {args['--account']}...\n") 238 | return smart_asa_account_freeze( 239 | smart_asa_contract=contract, 240 | smart_asa_app=smart_asa_app, 241 | freezer=freezer, 242 | freeze_asset=args[""], 243 | account_frozen=args[""], 244 | target_account=args["--account"], 245 | ) 246 | 247 | 248 | def asset_optin( 249 | args: dict, 250 | contract: Contract, 251 | smart_asa_app: AppAccount, 252 | ) -> None: 253 | account = Sandbox.from_public_key(args[""]) 254 | 255 | print(f"\n --- Opt-in Smart ASA {args['']}...") 256 | smart_asa_optin( 257 | smart_asa_contract=contract, 258 | smart_asa_app=smart_asa_app, 259 | asset_id=args[""], 260 | caller=account, 261 | ) 262 | print(f"\n --- Smart ASA {args['']} state:") 263 | return print(account.app_local_state(smart_asa_app.app_id), "\n") 264 | 265 | 266 | def asset_optout( 267 | args: dict, 268 | contract: Contract, 269 | smart_asa_app: AppAccount, 270 | ) -> None: 271 | account = Sandbox.from_public_key(args[""]) 272 | close_to = Account(address=args[""]) 273 | 274 | print(f"\n --- Closing Smart ASA {args['']}...") 275 | smart_asa_closeout( 276 | smart_asa_contract=contract, 277 | smart_asa_app=smart_asa_app, 278 | asset_id=args[""], 279 | caller=account, 280 | close_to=close_to, 281 | ) 282 | return print(f"\n --- Smart ASA {args['']} closed!") 283 | 284 | 285 | def asset_send( 286 | args: dict, 287 | contract: Contract, 288 | smart_asa_app: AppAccount, 289 | ) -> None: 290 | if args["--reserve"]: 291 | caller = Sandbox.from_public_key(args["--reserve"]) 292 | if ( 293 | args[""] == args["--reserve"] 294 | and args[""] == smart_asa_app.address 295 | ): 296 | action = "Minting" 297 | elif ( 298 | args[""] == smart_asa_app.address 299 | and args[""] == args["--reserve"] 300 | ): 301 | action = "Burning" 302 | else: 303 | action = "Sending" 304 | elif args["--clawback"]: 305 | caller = Sandbox.from_public_key(args["--clawback"]) 306 | action = "Clawbacking" 307 | else: 308 | caller = Sandbox.from_public_key(args[""]) 309 | action = "Sending" 310 | 311 | print( 312 | f"\n --- {action} {args['']} units of Smart ASA " 313 | f"{args['']} from {args['']} to " 314 | f"{args['']}..." 315 | ) 316 | smart_asa_transfer( 317 | smart_asa_contract=contract, 318 | smart_asa_app=smart_asa_app, 319 | xfer_asset=args[""], 320 | asset_amount=args[""], 321 | caller=caller, 322 | asset_receiver=args[""], 323 | asset_sender=args[""], 324 | ) 325 | return print(f" --- Confirmed!\n") 326 | 327 | 328 | def asset_or_account_info( 329 | args: dict, 330 | smart_asa_app: AppAccount, 331 | ) -> None: 332 | if args["--account"]: 333 | account = Account(address=args["--account"]) 334 | print(f"\n --- Smart ASA {args['']} state:") 335 | return print(account.app_local_state(smart_asa_app.app_id), "\n") 336 | else: 337 | return smart_asa_info(args[""]) 338 | 339 | 340 | def asset_get( 341 | args: dict, 342 | contract: Contract, 343 | smart_asa_app: AppAccount, 344 | ) -> None: 345 | caller = Sandbox.from_public_key(args[""]) 346 | result = smart_asa_get( 347 | smart_asa_contract=contract, 348 | smart_asa_app=smart_asa_app, 349 | caller=caller, 350 | asset_id=args[""], 351 | getter=args[""], 352 | account=args["--account"], 353 | ) 354 | return print( 355 | f"\n --- Smart ASA {args['']} " f"{args['']}: " f"{result}\n" 356 | ) 357 | 358 | 359 | def smart_asa_cli(): 360 | if len(sys.argv) == 1: 361 | # Display help if no arguments, see: 362 | # https://github.com/docopt/docopt/issues/420#issuecomment-405018014 363 | sys.argv.append("--help") 364 | 365 | args = docopt(__doc__) 366 | args = args_types(args) 367 | 368 | approval, clear, contract = smart_asa_abi.build_program() 369 | approval = compile_stateful(approval) 370 | clear = compile_stateful(clear) 371 | 372 | if args["create"]: 373 | return asset_create(args, approval, clear, contract) 374 | else: 375 | smart_asa = get_smart_asa_params(Sandbox.algod_client, args[""]) 376 | smart_asa_app = AppAccount.from_app_id(app_id=smart_asa["app_id"]) 377 | 378 | if args["config"]: 379 | return asset_config(args, contract, smart_asa_app) 380 | elif args["destroy"]: 381 | return asset_destroy(args, contract, smart_asa_app) 382 | elif args["freeze"]: 383 | return asset_or_account_freeze(args, contract, smart_asa_app) 384 | elif args["optin"]: 385 | return asset_optin(args, contract, smart_asa_app) 386 | elif args["optout"]: 387 | return asset_optout(args, contract, smart_asa_app) 388 | elif args["send"]: 389 | return asset_send(args, contract, smart_asa_app) 390 | elif args["info"]: 391 | return asset_or_account_info(args, smart_asa_app) 392 | elif args["get"]: 393 | return asset_get(args, contract, smart_asa_app) 394 | else: 395 | return print("\n --- Wrong command. Enter --help for CLI usage!\n") 396 | 397 | 398 | if __name__ == "__main__": 399 | smart_asa_cli() 400 | -------------------------------------------------------------------------------- /smart_asa_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Smart ASA client 3 | """ 4 | 5 | __author__ = "Cosimo Bassi, Stefano De Angelis" 6 | __email__ = ", " 7 | 8 | from typing import Any, Optional, Union 9 | from algosdk.abi import Contract 10 | from algosdk.atomic_transaction_composer import TransactionWithSigner 11 | from algosdk.v2client.algod import AlgodClient 12 | from algosdk.encoding import encode_address 13 | from algosdk.future.transaction import AssetTransferTxn, OnComplete 14 | from account import Account, AppAccount 15 | from utils import get_params, normalize_getter_params 16 | 17 | from smart_asa_asc import ( 18 | SMART_ASA_APP_BINDING, 19 | UNDERLYING_ASA_TOTAL, 20 | GlobalState, 21 | LocalState, 22 | ) 23 | 24 | 25 | def get_smart_asa_params(algod_client: AlgodClient, smart_asa_id: int) -> dict: 26 | smart_asa = algod_client.asset_info(smart_asa_id)["params"] 27 | assert SMART_ASA_APP_BINDING in smart_asa["url"] 28 | smart_asa_app_id = int(smart_asa["url"].replace(SMART_ASA_APP_BINDING, "")) 29 | smart_asa_app_account = AppAccount.from_app_id( 30 | app_id=smart_asa_app_id, 31 | algod_client=algod_client, 32 | ) 33 | smart_asa_state = smart_asa_app_account.global_state() 34 | smart_asa_app = algod_client.application_info(smart_asa_app_id)["params"] 35 | circulating_supply = UNDERLYING_ASA_TOTAL.value - smart_asa_app_account.asa_balance( 36 | smart_asa_id 37 | ) 38 | return { 39 | "smart_asa_id": smart_asa_id, 40 | "app_id": smart_asa_app_id, 41 | "app_address": smart_asa_app_account.address, 42 | "creator_addr": smart_asa_app["creator"], 43 | "circulating_supply": circulating_supply, 44 | "unit_name": smart_asa_state["unit_name"].decode(), 45 | "name": smart_asa_state["name"].decode(), 46 | "url": smart_asa_state["url"].decode(), 47 | "metadata_hash": smart_asa_state["metadata_hash"], 48 | "total": int(smart_asa_state["total"]), 49 | "decimals": int(smart_asa_state["decimals"]), 50 | "frozen": bool(smart_asa_state["frozen"]), 51 | "default_frozen": bool(smart_asa_state["default_frozen"]), 52 | "manager_addr": encode_address(smart_asa_state["manager_addr"]), 53 | "reserve_addr": encode_address(smart_asa_state["reserve_addr"]), 54 | "freeze_addr": encode_address(smart_asa_state["freeze_addr"]), 55 | "clawback_addr": encode_address(smart_asa_state["clawback_addr"]), 56 | } 57 | 58 | 59 | def smart_asa_app_create( 60 | teal_approval: str, teal_clear: str, creator: Account 61 | ) -> AppAccount: 62 | return creator.create_asc( 63 | approval_program=teal_approval, 64 | clear_program=teal_clear, 65 | global_schema=GlobalState.schema(), 66 | local_schema=LocalState.schema(), 67 | ) 68 | 69 | 70 | def smart_asa_create( 71 | smart_asa_contract: Contract, 72 | smart_asa_app: AppAccount, 73 | creator: Account, 74 | total: int, 75 | decimals: int = 0, 76 | default_frozen: bool = False, 77 | unit_name: str = "", 78 | name: str = "", 79 | url: str = "", 80 | metadata_hash: bytes = b"", 81 | manager_addr: Optional[Union[str, Account]] = None, 82 | reserve_addr: Optional[Union[str, Account]] = None, 83 | freeze_addr: Optional[Union[str, Account]] = None, 84 | clawback_addr: Optional[Union[str, Account]] = None, 85 | save_abi_call: Optional[str] = None, 86 | ) -> int: 87 | 88 | params = get_params(creator.algod_client) 89 | abi_call_fee = params.fee * 2 90 | 91 | return creator.abi_call( 92 | smart_asa_contract.get_method_by_name("asset_create"), 93 | total, 94 | decimals, 95 | default_frozen, 96 | unit_name, 97 | name, 98 | url, 99 | metadata_hash, 100 | manager_addr if manager_addr is not None else creator, 101 | reserve_addr if reserve_addr is not None else creator, 102 | freeze_addr if freeze_addr is not None else creator, 103 | clawback_addr if clawback_addr is not None else creator, 104 | app=smart_asa_app, 105 | fee=abi_call_fee, 106 | save_abi_call=save_abi_call, 107 | ) 108 | 109 | 110 | def smart_asa_optin( 111 | smart_asa_contract: Contract, 112 | smart_asa_app: AppAccount, 113 | asset_id: int, 114 | caller: Account, 115 | debug_txn: Optional[TransactionWithSigner] = None, 116 | save_abi_call: Optional[str] = None, 117 | ) -> None: 118 | 119 | params = get_params(caller.algod_client) 120 | abi_call_fee = params.fee 121 | 122 | if debug_txn: 123 | asa_optin_txn = debug_txn 124 | else: 125 | asa_optin_txn = AssetTransferTxn( 126 | sender=caller.address, 127 | sp=params, 128 | receiver=caller.address, 129 | amt=0, 130 | index=asset_id, 131 | ) 132 | asa_optin_txn = TransactionWithSigner( 133 | txn=asa_optin_txn, 134 | signer=caller, 135 | ) 136 | 137 | caller.abi_call( 138 | smart_asa_contract.get_method_by_name("asset_app_optin"), 139 | asset_id, 140 | asa_optin_txn, 141 | on_complete=OnComplete.OptInOC, 142 | app=smart_asa_app, 143 | fee=abi_call_fee, 144 | save_abi_call=save_abi_call, 145 | ) 146 | 147 | 148 | def smart_asa_closeout( 149 | smart_asa_contract: Contract, 150 | smart_asa_app: AppAccount, 151 | asset_id: int, 152 | caller: Account, 153 | close_to: Union[str, Account], 154 | debug_txn: Optional[TransactionWithSigner] = None, 155 | save_abi_call: Optional[str] = None, 156 | ) -> None: 157 | 158 | params = get_params(caller.algod_client) 159 | abi_call_fee = params.fee * 2 160 | 161 | if debug_txn: 162 | asa_close_to_txn = debug_txn 163 | else: 164 | asa_close_to_txn = AssetTransferTxn( 165 | sender=caller.address, 166 | sp=params, 167 | receiver=caller.address, 168 | amt=0, 169 | index=asset_id, 170 | close_assets_to=smart_asa_app.address, 171 | ) 172 | asa_close_to_txn = TransactionWithSigner( 173 | txn=asa_close_to_txn, 174 | signer=caller, 175 | ) 176 | 177 | caller.abi_call( 178 | smart_asa_contract.get_method_by_name("asset_app_closeout"), 179 | asset_id, 180 | close_to, 181 | on_complete=OnComplete.CloseOutOC, 182 | app=smart_asa_app, 183 | fee=abi_call_fee, 184 | group_extra_txns=[asa_close_to_txn], 185 | save_abi_call=save_abi_call, 186 | ) 187 | 188 | 189 | def smart_asa_config( 190 | smart_asa_contract: Contract, 191 | smart_asa_app: AppAccount, 192 | manager: Account, 193 | asset_id: int, 194 | config_total: Optional[int] = None, 195 | config_decimals: Optional[int] = None, 196 | config_default_frozen: Optional[bool] = None, 197 | config_unit_name: Optional[str] = None, 198 | config_name: Optional[str] = None, 199 | config_url: Optional[str] = None, 200 | config_metadata_hash: Optional[bytes] = None, 201 | config_manager_addr: Optional[Union[str, Account]] = None, 202 | config_reserve_addr: Optional[Union[str, Account]] = None, 203 | config_freeze_addr: Optional[Union[str, Account]] = None, 204 | config_clawback_addr: Optional[Union[str, Account]] = None, 205 | save_abi_call: Optional[str] = None, 206 | ) -> int: 207 | 208 | s_asa = get_smart_asa_params(manager.algod_client, asset_id) 209 | if config_metadata_hash is None: 210 | smart_asa_params = normalize_getter_params( 211 | smart_asa_get( 212 | smart_asa_contract=smart_asa_contract, 213 | smart_asa_app=smart_asa_app, 214 | caller=manager, 215 | asset_id=asset_id, 216 | getter="get_asset_config", 217 | ) 218 | ) 219 | config_metadata_hash = bytes(smart_asa_params.metadata_hash) 220 | 221 | if config_manager_addr is None: 222 | config_manager_addr = Account(address=s_asa["manager_addr"]) 223 | if config_reserve_addr is None: 224 | config_reserve_addr = Account(address=s_asa["reserve_addr"]) 225 | if config_freeze_addr is None: 226 | config_freeze_addr = Account(address=s_asa["freeze_addr"]) 227 | if config_clawback_addr is None: 228 | config_clawback_addr = Account(address=s_asa["clawback_addr"]) 229 | 230 | params = get_params(manager.algod_client) 231 | abi_call_fee = params.fee * 2 232 | 233 | manager.abi_call( 234 | smart_asa_contract.get_method_by_name("asset_config"), 235 | asset_id, 236 | s_asa["total"] if config_total is None else config_total, 237 | s_asa["decimals"] if config_decimals is None else config_decimals, 238 | s_asa["default_frozen"] 239 | if config_default_frozen is None 240 | else config_default_frozen, 241 | s_asa["unit_name"] if config_unit_name is None else config_unit_name, 242 | s_asa["name"] if config_name is None else config_name, 243 | s_asa["url"] if config_url is None else config_url, 244 | config_metadata_hash, 245 | config_manager_addr, 246 | config_reserve_addr, 247 | config_freeze_addr, 248 | config_clawback_addr, 249 | app=smart_asa_app, 250 | fee=abi_call_fee, 251 | save_abi_call=save_abi_call, 252 | ) 253 | return asset_id 254 | 255 | 256 | def smart_asa_transfer( 257 | smart_asa_contract: Contract, 258 | smart_asa_app: AppAccount, 259 | xfer_asset: int, 260 | asset_amount: int, 261 | caller: Account, 262 | asset_receiver: Account, 263 | asset_sender: Optional[Union[str, Account]] = None, 264 | save_abi_call: Optional[str] = None, 265 | ) -> None: 266 | 267 | params = get_params(caller.algod_client) 268 | abi_call_fee = params.fee * 2 269 | 270 | caller.abi_call( 271 | smart_asa_contract.get_method_by_name("asset_transfer"), 272 | xfer_asset, 273 | asset_amount, 274 | caller if asset_sender is None else asset_sender, 275 | asset_receiver, 276 | app=smart_asa_app, 277 | fee=abi_call_fee, 278 | save_abi_call=save_abi_call, 279 | ) 280 | 281 | 282 | def smart_asa_freeze( 283 | smart_asa_contract: Contract, 284 | smart_asa_app: AppAccount, 285 | freezer: Account, 286 | freeze_asset: int, 287 | asset_frozen: bool = False, 288 | save_abi_call: Optional[str] = None, 289 | ) -> None: 290 | 291 | params = get_params(freezer.algod_client) 292 | abi_call_fee = params.fee * 2 293 | 294 | freezer.abi_call( 295 | smart_asa_contract.get_method_by_name("asset_freeze"), 296 | freeze_asset, 297 | asset_frozen, 298 | app=smart_asa_app, 299 | fee=abi_call_fee, 300 | save_abi_call=save_abi_call, 301 | ) 302 | 303 | 304 | def smart_asa_account_freeze( 305 | smart_asa_contract: Contract, 306 | smart_asa_app: AppAccount, 307 | freezer: Account, 308 | freeze_asset: int, 309 | target_account: Account, 310 | account_frozen: bool = False, 311 | save_abi_call: Optional[str] = None, 312 | ) -> None: 313 | 314 | params = get_params(freezer.algod_client) 315 | abi_call_fee = params.fee * 2 316 | 317 | freezer.abi_call( 318 | smart_asa_contract.get_method_by_name("account_freeze"), 319 | freeze_asset, 320 | target_account, 321 | account_frozen, 322 | app=smart_asa_app, 323 | fee=abi_call_fee, 324 | save_abi_call=save_abi_call, 325 | ) 326 | 327 | 328 | def smart_asa_destroy( 329 | smart_asa_contract: Contract, 330 | smart_asa_app: AppAccount, 331 | manager: Account, 332 | destroy_asset: int, 333 | save_abi_call: Optional[str] = None, 334 | ) -> None: 335 | 336 | params = get_params(manager.algod_client) 337 | abi_call_fee = params.fee * 2 338 | 339 | manager.abi_call( 340 | smart_asa_contract.get_method_by_name("asset_destroy"), 341 | destroy_asset, 342 | app=smart_asa_app, 343 | fee=abi_call_fee, 344 | save_abi_call=save_abi_call, 345 | ) 346 | 347 | 348 | def smart_asa_get( 349 | smart_asa_contract: Contract, 350 | smart_asa_app: AppAccount, 351 | caller: Account, 352 | asset_id: int, 353 | getter: str, 354 | account: Optional[Union[str, Account]] = None, 355 | save_abi_call: Optional[str] = None, 356 | ) -> Any: 357 | args = [asset_id] 358 | if account is not None: 359 | args.append(account) 360 | return caller.abi_call( 361 | smart_asa_contract.get_method_by_name(getter), 362 | *args, 363 | app=smart_asa_app, 364 | save_abi_call=save_abi_call, 365 | ) 366 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from collections import namedtuple 3 | from inspect import get_annotations 4 | from typing import Union 5 | from algosdk import constants 6 | from algosdk.future import transaction 7 | from algosdk.v2client import algod 8 | from smart_asa_asc import SmartASAConfig as PyTealSmartASAConfig 9 | 10 | 11 | def decode_state(state) -> dict[str, Union[int, bytes]]: 12 | return { 13 | # We are assuming that global space `key` are printable. 14 | # If that's not necessarily true, we can change that. 15 | base64.b64decode(s["key"]).decode(): base64.b64decode(s["value"]["bytes"]) 16 | if s["value"]["type"] == 1 17 | else int(s["value"]["uint"]) 18 | for s in state 19 | } 20 | 21 | 22 | def get_global_state( 23 | algod_client: algod.AlgodClient, asc_idx: int 24 | ) -> dict[str, Union[bytes, int]]: 25 | global_state = algod_client.application_info(asc_idx)["params"]["global-state"] 26 | global_state = decode_state(global_state) 27 | return global_state 28 | 29 | 30 | def get_local_state( 31 | algod_client: algod.AlgodClient, account_address: str, asc_idx: int 32 | ) -> dict[str, Union[bytes, int]]: 33 | local_states = algod_client.account_info(account_address)["apps-local-state"] 34 | local_state = [s for s in local_states if s["id"] == asc_idx][0].get( 35 | "key-value", {} 36 | ) 37 | local_state = decode_state(local_state) 38 | return local_state 39 | 40 | 41 | def get_params( 42 | algod_client: algod.AlgodClient, fee: int = None 43 | ) -> transaction.SuggestedParams: 44 | params = algod_client.suggested_params() 45 | params.flat_fee = True 46 | params.fee = fee or constants.MIN_TXN_FEE 47 | return params 48 | 49 | 50 | def get_last_round(algod_client: algod.AlgodClient): 51 | return algod_client.status()["last-round"] 52 | 53 | 54 | def get_last_timestamp(algod_client: algod.AlgodClient): 55 | return algod_client.block_info(get_last_round(algod_client))["block"]["ts"] 56 | 57 | 58 | def assemble_program(algod_client: algod.AlgodClient, source_code: str) -> bytes: 59 | compile_response = algod_client.compile(source_code) 60 | return base64.b64decode(compile_response["result"]) 61 | 62 | 63 | SmartASAConfig = namedtuple( 64 | PyTealSmartASAConfig.__class__.__name__, 65 | list(get_annotations(PyTealSmartASAConfig)), 66 | ) 67 | 68 | 69 | def normalize_getter_params(getter_params: list) -> SmartASAConfig: 70 | return SmartASAConfig(*getter_params) 71 | --------------------------------------------------------------------------------