├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── NOTICE ├── README.md ├── contracts ├── cw20-atomic-swap │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ ├── NOTICE │ ├── README.md │ ├── examples │ │ └── schema.rs │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw20-bonding │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ ├── NOTICE │ ├── README.md │ ├── examples │ │ └── schema.rs │ └── src │ │ ├── contract.rs │ │ ├── curves.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw20-escrow │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ ├── NOTICE │ ├── README.md │ ├── examples │ │ └── schema.rs │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── integration_test.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw20-merkle-airdrop │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ ├── NOTICE │ ├── README.md │ ├── examples │ │ └── schema.rs │ ├── helpers │ │ ├── .eslintignore │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── README.md │ │ ├── bin │ │ │ ├── run │ │ │ └── run.cmd │ │ ├── package.json │ │ ├── src │ │ │ ├── airdrop.ts │ │ │ ├── commands │ │ │ │ ├── generateProofs.ts │ │ │ │ ├── generateRoot.ts │ │ │ │ └── verifyProofs.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── yarn.lock │ ├── src │ │ ├── contract.rs │ │ ├── enumerable.rs │ │ ├── error.rs │ │ ├── helpers.rs │ │ ├── lib.rs │ │ ├── migrations.rs │ │ ├── msg.rs │ │ └── state.rs │ └── testdata │ │ ├── airdrop_external_sig_list.json │ │ ├── airdrop_external_sig_test_data.json │ │ ├── airdrop_stage_1_list.json │ │ ├── airdrop_stage_1_test_data.json │ │ ├── airdrop_stage_1_test_multi_data.json │ │ ├── airdrop_stage_2_list.json │ │ └── airdrop_stage_2_test_data.json ├── cw20-staking │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ ├── NOTICE │ ├── README.md │ ├── examples │ │ └── schema.rs │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs └── cw20-streams │ ├── .cargo │ └── config │ ├── Cargo.toml │ ├── NOTICE │ ├── README.md │ ├── examples │ └── schema.rs │ └── src │ ├── contract.rs │ ├── error.rs │ ├── lib.rs │ ├── msg.rs │ └── state.rs └── scripts ├── publish.sh └── set_version.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | workflows: 3 | version: 2 4 | test: 5 | jobs: 6 | - contract_cw20_atomic_swap 7 | - contract_cw20_bonding 8 | - contract_cw20_escrow 9 | - contract_cw20_staking 10 | - contract_cw20_merkle_airdrop 11 | - contract_cw20_streams 12 | - lint 13 | - wasm-build 14 | deploy: 15 | jobs: 16 | - build_and_upload_contracts: 17 | filters: 18 | tags: 19 | only: /^v[0-9]+\.[0-9]+\.[0-9]+.*/ 20 | branches: 21 | ignore: /.*/ 22 | - build_and_upload_schemas: 23 | filters: 24 | tags: 25 | only: /^v[0-9]+\.[0-9]+\.[0-9]+.*/ 26 | branches: 27 | ignore: /.*/ 28 | 29 | jobs: 30 | contract_cw20_atomic_swap: 31 | docker: 32 | - image: rust:1.64.0 33 | working_directory: ~/project/contracts/cw20-atomic-swap 34 | steps: 35 | - checkout: 36 | path: ~/project 37 | - run: 38 | name: Version information 39 | command: rustc --version; cargo --version; rustup --version 40 | - restore_cache: 41 | keys: 42 | - cargocache-cw20-atomic-swap-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 43 | - run: 44 | name: Add wasm32 target 45 | command: rustup target add wasm32-unknown-unknown 46 | - run: 47 | name: Unit Tests 48 | environment: 49 | RUST_BACKTRACE: 1 50 | command: cargo unit-test --locked 51 | - run: 52 | name: Build and run schema generator 53 | command: cargo schema --locked 54 | - save_cache: 55 | paths: 56 | - /usr/local/cargo/registry 57 | - target 58 | key: cargocache-cw20-atomic-swap-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 59 | 60 | contract_cw20_bonding: 61 | docker: 62 | - image: rust:1.64.0 63 | working_directory: ~/project/contracts/cw20-bonding 64 | steps: 65 | - checkout: 66 | path: ~/project 67 | - run: 68 | name: Version information 69 | command: rustc --version; cargo --version; rustup --version 70 | - restore_cache: 71 | keys: 72 | - cargocache-cw20-bonding-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 73 | - run: 74 | name: Unit Tests 75 | environment: 76 | RUST_BACKTRACE: 1 77 | command: cargo unit-test --locked 78 | - run: 79 | name: Build and run schema generator 80 | command: cargo schema --locked 81 | - save_cache: 82 | paths: 83 | - /usr/local/cargo/registry 84 | - target 85 | key: cargocache-cw20-bonding-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 86 | 87 | contract_cw20_escrow: 88 | docker: 89 | - image: rust:1.64.0 90 | working_directory: ~/project/contracts/cw20-escrow 91 | steps: 92 | - checkout: 93 | path: ~/project 94 | - run: 95 | name: Version information 96 | command: rustc --version; cargo --version; rustup --version 97 | - restore_cache: 98 | keys: 99 | - cargocache-cw20-escrow-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 100 | - run: 101 | name: Unit Tests 102 | environment: 103 | RUST_BACKTRACE: 1 104 | command: cargo unit-test --locked 105 | - run: 106 | name: Build and run schema generator 107 | command: cargo schema --locked 108 | - save_cache: 109 | paths: 110 | - /usr/local/cargo/registry 111 | - target 112 | key: cargocache-cw20-escrow-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 113 | 114 | contract_cw20_staking: 115 | docker: 116 | - image: rust:1.64.0 117 | working_directory: ~/project/contracts/cw20-staking 118 | steps: 119 | - checkout: 120 | path: ~/project 121 | - run: 122 | name: Version information 123 | command: rustc --version; cargo --version; rustup --version 124 | - restore_cache: 125 | keys: 126 | - cargocache-cw20-staking-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 127 | - run: 128 | name: Unit Tests 129 | environment: 130 | RUST_BACKTRACE: 1 131 | command: cargo unit-test --locked 132 | - run: 133 | name: Build and run schema generator 134 | command: cargo schema --locked 135 | - save_cache: 136 | paths: 137 | - /usr/local/cargo/registry 138 | - target 139 | key: cargocache-cw20-staking-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 140 | 141 | contract_cw20_merkle_airdrop: 142 | docker: 143 | - image: rust:1.64.0 144 | working_directory: ~/project/contracts/cw20-merkle-airdrop 145 | steps: 146 | - checkout: 147 | path: ~/project 148 | - run: 149 | name: Version information 150 | command: rustc --version; cargo --version; rustup --version 151 | - restore_cache: 152 | keys: 153 | - cargocache-cw20-merkle-airdrop-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 154 | - run: 155 | name: Unit Tests 156 | environment: 157 | RUST_BACKTRACE: 1 158 | command: cargo unit-test --locked 159 | - run: 160 | name: Build and run schema generator 161 | command: cargo schema --locked 162 | - save_cache: 163 | paths: 164 | - /usr/local/cargo/registry 165 | - target 166 | key: cargocache-cw20-merkle-airdrop-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 167 | 168 | contract_cw20_streams: 169 | docker: 170 | - image: rust:1.64.0 171 | working_directory: ~/project/contracts/cw20-streams 172 | steps: 173 | - checkout: 174 | path: ~/project 175 | - run: 176 | name: Version information 177 | command: rustc --version; cargo --version; rustup --version 178 | - restore_cache: 179 | keys: 180 | - cargocache-cw20-streams-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 181 | - run: 182 | name: Unit Tests 183 | environment: 184 | RUST_BACKTRACE: 1 185 | command: cargo unit-test --locked 186 | - run: 187 | name: Build and run schema generator 188 | command: cargo schema --locked 189 | - save_cache: 190 | paths: 191 | - /usr/local/cargo/registry 192 | - target 193 | key: cargocache-cw20-streams-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 194 | 195 | lint: 196 | docker: 197 | - image: rust:1.64.0 198 | steps: 199 | - checkout 200 | - run: 201 | name: Version information 202 | command: rustc --version; cargo --version; rustup --version; rustup target list --installed 203 | - restore_cache: 204 | keys: 205 | - cargocache-v2-lint-rust:1.64.0-{{ checksum "Cargo.lock" }} 206 | - run: 207 | name: Add rustfmt component 208 | command: rustup component add rustfmt 209 | - run: 210 | name: Add clippy component 211 | command: rustup component add clippy 212 | - run: 213 | name: Check formatting of workspace 214 | command: cargo fmt -- --check 215 | - run: 216 | name: Clippy linting on workspace 217 | command: cargo clippy --all-targets -- -D warnings 218 | - save_cache: 219 | paths: 220 | - /usr/local/cargo/registry 221 | - target/debug/.fingerprint 222 | - target/debug/build 223 | - target/debug/deps 224 | key: cargocache-v2-lint-rust:1.64.0-{{ checksum "Cargo.lock" }} 225 | 226 | # This runs one time on the top level to ensure all contracts compile properly into wasm. 227 | # We don't run the wasm build per contract build, and then reuse a lot of the same dependencies, so this speeds up CI time 228 | # for all the other tests. 229 | # We also sanity-check the resultant wasm files. 230 | wasm-build: 231 | docker: 232 | - image: rust:1.64.0 233 | steps: 234 | - checkout: 235 | path: ~/project 236 | - run: 237 | name: Version information 238 | command: rustc --version; cargo --version; rustup --version 239 | - restore_cache: 240 | keys: 241 | - cargocache-wasm-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 242 | - run: 243 | name: Add wasm32 target 244 | command: rustup target add wasm32-unknown-unknown 245 | - run: 246 | name: Build Wasm Release 247 | command: | 248 | for C in ./contracts/*/ 249 | do 250 | echo "Compiling `basename $C`..." 251 | (cd $C && cargo build --release --target wasm32-unknown-unknown --locked) 252 | done 253 | - run: 254 | name: Install check_contract 255 | # Uses --debug for compilation speed 256 | command: cargo install --debug --version 1.0.0 --features iterator --example check_contract -- cosmwasm-vm 257 | - save_cache: 258 | paths: 259 | - /usr/local/cargo/registry 260 | - target 261 | key: cargocache-wasm-rust:1.64.0-{{ checksum "~/project/Cargo.lock" }} 262 | - run: 263 | name: Check wasm contracts 264 | command: | 265 | for W in ./target/wasm32-unknown-unknown/release/*.wasm 266 | do 267 | echo -n "Checking `basename $W`... " 268 | check_contract $W 269 | done 270 | 271 | # This job roughly follows the instructions from https://circleci.com/blog/publishing-to-github-releases-via-circleci/ 272 | build_and_upload_contracts: 273 | docker: 274 | # Image from https://github.com/cibuilds/github, based on alpine 275 | - image: cibuilds/github:0.13 276 | steps: 277 | - run: 278 | name: Install Docker client 279 | command: apk add docker-cli 280 | - setup_remote_docker 281 | - checkout 282 | - run: 283 | # We cannot mount local folders, see https://circleci.com/docs/2.0/building-docker-images/#mounting-folders 284 | name: Prepare volume with source code 285 | command: | 286 | # create a dummy container which will hold a volume with config 287 | docker create -v /code --name with_code alpine /bin/true 288 | # copy a config file into this volume 289 | docker cp Cargo.toml with_code:/code 290 | docker cp Cargo.lock with_code:/code 291 | # copy code into this volume 292 | docker cp ./contracts with_code:/code 293 | docker cp ./packages with_code:/code 294 | - run: 295 | name: Build development contracts 296 | command: | 297 | docker run --volumes-from with_code cosmwasm/workspace-optimizer:0.12.4 298 | docker cp with_code:/code/artifacts ./artifacts 299 | - run: 300 | name: Show data 301 | command: | 302 | ls -l artifacts 303 | cat artifacts/checksums.txt 304 | - run: 305 | name: Publish artifacts on GitHub 306 | command: | 307 | TAG="$CIRCLE_TAG" 308 | TITLE="$TAG" 309 | BODY="Attached there are some build artifacts generated at this tag. Those are for development purposes only! Please use crates.io to find the packages of this release." 310 | ghr -t "$GITHUB_TOKEN" \ 311 | -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" \ 312 | -c "$CIRCLE_SHA1" \ 313 | -n "$TITLE" -b "$BODY" \ 314 | -replace \ 315 | "$TAG" ./artifacts/ 316 | 317 | build_and_upload_schemas: 318 | docker: 319 | - image: rust:1.64.0 320 | working_directory: ~/project 321 | steps: 322 | - checkout: 323 | path: ~/project 324 | - run: 325 | name: Create schemas directory 326 | command: mkdir -p schemas 327 | - run: 328 | name: Install ghr 329 | command: wget https://github.com/tcnksm/ghr/releases/download/v0.14.0/ghr_v0.14.0_linux_amd64.tar.gz -O - | tar -zxvf - -C /usr/local/bin --wildcards --strip-components 1 */ghr 330 | - run: 331 | name: Build and run schema generator for packages 332 | command: | 333 | for S in ./packages/*/examples/schema.rs 334 | do 335 | P=$(dirname $S)/.. 336 | echo "Generating schema for $P ..." 337 | (cd $P && cargo schema --locked && tar -zcf ~/project/schemas/$(basename $(pwd))_schema.tar.gz ./schema) 338 | done 339 | - run: 340 | name: Build and run schema generator for contracts 341 | command: | 342 | for C in ./contracts/*/ 343 | do 344 | echo "Generating schema for $C ..." 345 | (cd $C && cargo schema --locked && tar -zcf ~/project/schemas/$(basename $(pwd))_schema.tar.gz ./schema) 346 | done 347 | - run: 348 | name: Show data 349 | command: ls -l ./schemas 350 | - run: 351 | name: Publish schemas on GitHub 352 | command: | 353 | TAG="$CIRCLE_TAG" 354 | TITLE="$TAG" 355 | BODY="Attached there are some schemas and build artifacts generated at this tag. Those are for development purposes only! Please use crates.io to find the packages of this release." 356 | ghr -t "$GITHUB_TOKEN" \ 357 | -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" \ 358 | -c "$CIRCLE_SHA1" \ 359 | -n "$TITLE" -b "$BODY" \ 360 | -replace \ 361 | "$TAG" ./schemas/ 362 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 120 11 | 12 | [*.rs] 13 | indent_size = 4 14 | max_line_length = 100 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Text file backups 5 | **/*.rs.bk 6 | 7 | # Build results 8 | target/ 9 | 10 | # IDEs 11 | .vscode/ 12 | .idea/ 13 | *.iml 14 | 15 | # Auto-gen 16 | .cargo-ok 17 | 18 | # Build artifacts 19 | *.wasm 20 | hash.txt 21 | contracts.txt 22 | artifacts/ 23 | 24 | # code coverage 25 | tarpaulin-report.* 26 | 27 | packages/*/schema 28 | contracts/*/schema 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["contracts/*"] 3 | 4 | [profile.release.package.cw20-atomic-swap] 5 | codegen-units = 1 6 | incremental = false 7 | 8 | [profile.release.package.cw20-bonding] 9 | codegen-units = 1 10 | incremental = false 11 | 12 | [profile.release.package.cw20-escrow] 13 | codegen-units = 1 14 | incremental = false 15 | 16 | [profile.release.package.cw20-merkle-airdrop] 17 | codegen-units = 1 18 | incremental = false 19 | 20 | [profile.release.package.cw20-staking] 21 | codegen-units = 1 22 | incremental = false 23 | 24 | [profile.release.package.cw20-streams] 25 | codegen-units = 1 26 | incremental = false 27 | 28 | [profile.release] 29 | rpath = false 30 | lto = true 31 | overflow-checks = true 32 | opt-level = 3 33 | debug = false 34 | debug-assertions = false 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Cw-Tokens: Example contracts using cw20 standard 2 | Copyright (C) 2021-2022 Confio GmbH 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CosmWasm Tokens 2 | 3 | [![CircleCI](https://circleci.com/gh/CosmWasm/cw-plus/tree/master.svg?style=shield)](https://circleci.com/gh/CosmWasm/cw-plus/tree/master) 4 | 5 | This is a collection of [cw20-related](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md) contracts 6 | extracted from [cw-plus](https://github.com/CosmWasm/cw-plus). These serve as examples of what is possible to build 7 | and as starting points for your own CosmWasm token contracts. 8 | 9 | None of these have been audited or are considered ready-for-production as is. Contributions may come from many 10 | community members. Please do your own due dilligence on them before using on any production site, and please 11 | [raise Github issues](https://github.com/CosmWasm/cw-tokens/issues) for any bugs you find. 12 | 13 | You are more than welcome to [create a PR](https://github.com/CosmWasm/cw-tokens/pulls) to add any cw20-related 14 | contract you have written that you would like to share with the community. 15 | 16 | 17 | | Contracts | Download | Docs | 18 | | ----------------------- |-------------------------------------------------------------------------------------------------------------| -------------------------------------------------------------------------| 19 | | cw20-atomic-swap | [Release v0.14.2](https://github.com/CosmWasm/cw-tokens/releases/download/v0.14.2/cw20_atomic_swap.wasm) | [![Docs](https://docs.rs/cw20-atomic-swap/badge.svg)](https://docs.rs/cw20-atomic-swap) | 20 | | cw20-bonding | [Release v0.14.2](https://github.com/CosmWasm/cw-tokens/releases/download/v0.14.2/cw20_bonding.wasm) | [![Docs](https://docs.rs/cw20-bonding/badge.svg)](https://docs.rs/cw20-bonding) | 21 | | cw20-escrow | [Release v0.14.2](https://github.com/CosmWasm/cw-tokens/releases/download/v0.14.2/cw20_escrow.wasm) | [![Docs](https://docs.rs/cw20-escrow/badge.svg)](https://docs.rs/cw20-escrow) | 22 | | cw20-staking | [Release v0.14.2](https://github.com/CosmWasm/cw-tokens/releases/download/v0.14.2/cw20_staking.wasm) | [![Docs](https://docs.rs/cw20-staking/badge.svg)](https://docs.rs/cw20-staking) | 23 | | cw20-merkle-airdrop | [Release v0.14.2](https://github.com/CosmWasm/cw-tokens/releases/download/v0.14.2/cw20_merkle_airdrop.wasm) | [![Docs](https://docs.rs/cw20-merkle-airdrop/badge.svg)](https://docs.rs/cw20-merkle-airdrop) | 24 | 25 | **Warning** None of these contracts have been audited and no liability is 26 | assumed for the use of this code. They are provided to turbo-start 27 | your projects. 28 | 29 | 30 | ## Contracts 31 | 32 | All contracts add functionality around the CW20 Fungible Token standard: 33 | 34 | * [`cw20-atomic-swap`](./contracts/cw20-atomic-swap) an implementation of atomic swaps for 35 | both native and cw20 tokens. 36 | * [`cw20-bonding`](./contracts/cw20-bonding) a smart contract implementing arbitrary bonding curves, 37 | which can use native and cw20 tokens as reserve tokens. 38 | * [`cw20-staking`](./contracts/cw20-staking) provides staking derivatives, 39 | staking native tokens on your behalf and minting cw20 tokens that can 40 | be used to claim them. It uses `cw20-base` for all the cw20 logic and 41 | only implements the interactions with the staking module and accounting 42 | for prices. 43 | * [`cw20-escrow`](./contracts/cw20-escrow) is a basic escrow contract 44 | (arbiter can release or refund tokens) that is compatible with all native 45 | and cw20 tokens. This is a good example to show how to interact with 46 | cw20 tokens. 47 | * [`cw20-merkle-airdrop`](./contracts/cw20-merkle-airdrop) is a contract 48 | for efficient cw20 token airdrop distribution. 49 | 50 | ## Compiling 51 | 52 | To compile all the contracts, run the following in the repo root: 53 | 54 | ``` 55 | docker run --rm -v "$(pwd)":/code \ 56 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ 57 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 58 | cosmwasm/workspace-optimizer:0.12.8 59 | ``` 60 | 61 | This will compile all packages in the `contracts` directory and output the 62 | stripped and optimized wasm code under the `artifacts` directory as output, 63 | along with a `checksums.txt` file. 64 | 65 | If you hit any issues there and want to debug, you can try to run the 66 | following in each contract dir: 67 | `RUSTFLAGS="-C link-arg=-s" cargo build --release --target=wasm32-unknown-unknown --locked` 68 | 69 | ## Licenses 70 | 71 | All code in this repo will always be licensed under [Apache-2.0](./LICENSE). 72 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --example schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-atomic-swap" 3 | version = "0.14.2" 4 | authors = ["Mauro Lacy "] 5 | edition = "2018" 6 | description = "Implementation of Atomic Swaps" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-tokens" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | backtraces = ["cosmwasm-std/backtraces"] 17 | # use library feature to disable all instantiate/execute/query exports 18 | library = [] 19 | 20 | [dependencies] 21 | cw-utils = "0.16.0" 22 | cw2 = "0.16.0" 23 | cw20 = "0.16.0" 24 | cosmwasm-schema = "1.1.5" 25 | cosmwasm-std = "1.1.5" 26 | cw-storage-plus = "0.16.0" 27 | thiserror = "1.0.31" 28 | hex = "0.3.2" 29 | sha2 = "0.8.2" 30 | 31 | [dev-dependencies] 32 | 33 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/NOTICE: -------------------------------------------------------------------------------- 1 | Atomic-Swap: A CosmWasm atomic swap contract that handles native tokens 2 | Copyright (C) 2020-21 Confio OÜ 3 | Copyright (C) 2021-22 Confio GmbH 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/README.md: -------------------------------------------------------------------------------- 1 | # Atomic Swaps 2 | 3 | This is a contract that allows users to execute atomic swaps. 4 | It implements one side of an atomic swap. The other side can be realized 5 | by an equivalent contract in the same blockchain or, typically, on a different blockchain. 6 | 7 | Each side of an atomic swap has a sender, a recipient, a hash, 8 | and a timeout. It also has a unique id (for future calls to reference it). 9 | The hash is a sha256-encoded 32-bytes long phrase. 10 | The timeout can be either time-based (seconds since midnight, January 1, 1970), 11 | or block height based. 12 | 13 | The basic function is, the sender chooses a 32-bytes long phrase as preimage, hashes it, 14 | and then uses the hash to create a swap with funds. 15 | Before the timeout, anybody that knows the preimage may decide to release the funds 16 | to the original recipient. 17 | After the timeout (and if no release has been executed), anyone can refund 18 | the locked tokens to the original sender. 19 | On the other side of the swap the process is similar, with sender and recipient exchanged. 20 | The hash must be the same, so the first sender can claim the funds, revealing the preimage 21 | and triggering the swap. 22 | 23 | See the [IOV atomic swap spec](https://github.com/iov-one/iov-core/blob/master/docs/atomic-swap-protocol-v1.md) 24 | for details. 25 | 26 | ## Token types 27 | 28 | Currently native tokens are supported; an upcoming version will support CW20 tokens. 29 | 30 | ## Running this contract 31 | 32 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 33 | 34 | You can run unit tests on this via: 35 | 36 | `cargo test` 37 | 38 | Once you are happy with the content, you can compile it to wasm via: 39 | 40 | ``` 41 | RUSTFLAGS='-C link-arg=-s' cargo wasm 42 | cp ../../target/wasm32-unknown-unknown/release/cw20_atomic_swap.wasm . 43 | ls -l cw20_atomic_swap.wasm 44 | sha256sum cw20_atomic_swap.wasm 45 | ``` 46 | 47 | Or for a production-ready (optimized) build, run a build command in the 48 | the repository root: https://github.com/CosmWasm/cw-plus#compiling. 49 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | use cw20_atomic_swap::msg::ExecuteMsg; 3 | use cw20_atomic_swap::msg::InstantiateMsg; 4 | use cw20_atomic_swap::msg::QueryMsg; 5 | 6 | fn main() { 7 | write_api! { 8 | instantiate: InstantiateMsg, 9 | query: QueryMsg, 10 | execute: ExecuteMsg, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, PartialEq)] 5 | pub enum ContractError { 6 | #[error("{0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("Hash parse error: {0}")] 10 | ParseError(String), 11 | 12 | #[error("Invalid atomic swap id")] 13 | InvalidId {}, 14 | 15 | #[error("Invalid preimage")] 16 | InvalidPreimage {}, 17 | 18 | #[error("Invalid hash ({0} chars): must be 64 characters")] 19 | InvalidHash(usize), 20 | 21 | #[error("Send some coins to create an atomic swap")] 22 | EmptyBalance {}, 23 | 24 | #[error("Atomic swap not yet expired")] 25 | NotExpired, 26 | 27 | #[error("Expired atomic swap")] 28 | Expired, 29 | 30 | #[error("Atomic swap already exists")] 31 | AlreadyExists, 32 | } 33 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | mod error; 3 | pub mod msg; 4 | pub mod state; 5 | 6 | pub use crate::error::ContractError; 7 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | 3 | use cosmwasm_std::Coin; 4 | use cw20::{Cw20Coin, Cw20ReceiveMsg, Expiration}; 5 | 6 | #[cw_serde] 7 | pub struct InstantiateMsg {} 8 | 9 | #[cw_serde] 10 | pub enum ExecuteMsg { 11 | Create(CreateMsg), 12 | /// Release sends all tokens to the recipient. 13 | Release { 14 | id: String, 15 | /// This is the preimage, must be exactly 32 bytes in hex (64 chars) 16 | /// to release: sha256(from_hex(preimage)) == from_hex(hash) 17 | preimage: String, 18 | }, 19 | /// Refund returns all remaining tokens to the original sender, 20 | Refund { 21 | id: String, 22 | }, 23 | /// This accepts a properly-encoded ReceiveMsg from a cw20 contract 24 | Receive(Cw20ReceiveMsg), 25 | } 26 | 27 | #[cw_serde] 28 | pub enum ReceiveMsg { 29 | Create(CreateMsg), 30 | } 31 | 32 | #[cw_serde] 33 | pub struct CreateMsg { 34 | /// id is a human-readable name for the swap to use later. 35 | /// 3-20 bytes of utf-8 text 36 | pub id: String, 37 | /// This is hex-encoded sha-256 hash of the preimage (must be 32*2 = 64 chars) 38 | pub hash: String, 39 | /// If approved, funds go to the recipient 40 | pub recipient: String, 41 | /// You can set expiration at time or at block height the contract is valid at. 42 | /// After the contract is expired, it can be returned to the original funder. 43 | pub expires: Expiration, 44 | } 45 | 46 | pub fn is_valid_name(name: &str) -> bool { 47 | let bytes = name.as_bytes(); 48 | if bytes.len() < 3 || bytes.len() > 20 { 49 | return false; 50 | } 51 | true 52 | } 53 | 54 | #[cw_serde] 55 | #[derive(QueryResponses)] 56 | pub enum QueryMsg { 57 | /// Show all open swaps. Return type is ListResponse. 58 | #[returns(ListResponse)] 59 | List { 60 | start_after: Option, 61 | limit: Option, 62 | }, 63 | /// Returns the details of the named swap, error if not created. 64 | /// Return type: DetailsResponse. 65 | #[returns(DetailsResponse)] 66 | Details { id: String }, 67 | } 68 | 69 | #[cw_serde] 70 | pub struct ListResponse { 71 | /// List all open swap ids 72 | pub swaps: Vec, 73 | } 74 | 75 | #[cw_serde] 76 | pub struct DetailsResponse { 77 | /// Id of this swap 78 | pub id: String, 79 | /// This is hex-encoded sha-256 hash of the preimage (must be 32*2 = 64 chars) 80 | pub hash: String, 81 | /// If released, funds go to the recipient 82 | pub recipient: String, 83 | /// If refunded, funds go to the source 84 | pub source: String, 85 | /// Once a swap is expired, it can be returned to the original source (via "refund"). 86 | pub expires: Expiration, 87 | /// Balance in native tokens or cw20 token, with human-readable address 88 | pub balance: BalanceHuman, 89 | } 90 | 91 | #[cw_serde] 92 | pub enum BalanceHuman { 93 | Native(Vec), 94 | Cw20(Cw20Coin), 95 | } 96 | -------------------------------------------------------------------------------- /contracts/cw20-atomic-swap/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | 3 | use cosmwasm_std::{Addr, Binary, BlockInfo, Order, StdResult, Storage}; 4 | use cw_storage_plus::{Bound, Map}; 5 | 6 | use cw20::{Balance, Expiration}; 7 | 8 | #[cw_serde] 9 | pub struct AtomicSwap { 10 | /// This is the sha-256 hash of the preimage 11 | pub hash: Binary, 12 | pub recipient: Addr, 13 | pub source: Addr, 14 | pub expires: Expiration, 15 | /// Balance in native tokens, or cw20 token 16 | pub balance: Balance, 17 | } 18 | 19 | impl AtomicSwap { 20 | pub fn is_expired(&self, block: &BlockInfo) -> bool { 21 | self.expires.is_expired(block) 22 | } 23 | } 24 | 25 | pub const SWAPS: Map<&str, AtomicSwap> = Map::new("atomic_swap"); 26 | 27 | /// This returns the list of ids for all active swaps 28 | pub fn all_swap_ids<'a>( 29 | storage: &dyn Storage, 30 | start: Option>, 31 | limit: usize, 32 | ) -> StdResult> { 33 | SWAPS 34 | .keys(storage, start, None, Order::Ascending) 35 | .take(limit) 36 | .collect() 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | 43 | use cosmwasm_std::testing::MockStorage; 44 | use cosmwasm_std::Binary; 45 | 46 | #[test] 47 | fn test_no_swap_ids() { 48 | let storage = MockStorage::new(); 49 | let ids = all_swap_ids(&storage, None, 10).unwrap(); 50 | assert_eq!(0, ids.len()); 51 | } 52 | 53 | fn dummy_swap() -> AtomicSwap { 54 | AtomicSwap { 55 | recipient: Addr::unchecked("recip"), 56 | source: Addr::unchecked("source"), 57 | expires: Default::default(), 58 | hash: Binary("hash".into()), 59 | balance: Default::default(), 60 | } 61 | } 62 | 63 | #[test] 64 | fn test_all_swap_ids() { 65 | let mut storage = MockStorage::new(); 66 | SWAPS.save(&mut storage, "lazy", &dummy_swap()).unwrap(); 67 | SWAPS.save(&mut storage, "assign", &dummy_swap()).unwrap(); 68 | SWAPS.save(&mut storage, "zen", &dummy_swap()).unwrap(); 69 | 70 | let ids = all_swap_ids(&storage, None, 10).unwrap(); 71 | assert_eq!(3, ids.len()); 72 | assert_eq!( 73 | vec!["assign".to_string(), "lazy".to_string(), "zen".to_string()], 74 | ids 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --example schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-bonding" 3 | version = "0.14.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Implement basic bonding curve to issue cw20 tokens" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-tokens" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | 17 | [features] 18 | backtraces = ["cosmwasm-std/backtraces"] 19 | # use library feature to disable all instantiate/execute/query exports 20 | library = [] 21 | 22 | [dependencies] 23 | cw-utils = "0.16.0" 24 | cw2 = "0.16.0" 25 | cw20 = "0.16.0" 26 | cw20-base ={version = "0.16.0", features = ["library"]} 27 | cw-storage-plus = "0.16.0" 28 | cosmwasm-std = "1.1.5" 29 | cosmwasm-schema = "1.1.5" 30 | thiserror = "1.0.31" 31 | rust_decimal = "1.14.3" 32 | integer-sqrt = "0.1.5" 33 | integer-cbrt = "0.1.2" 34 | 35 | [dev-dependencies] 36 | 37 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/NOTICE: -------------------------------------------------------------------------------- 1 | CW20-Bonding: Bonding Curve to release CW20 token 2 | Copyright 2020-21 Ethan Frey 3 | Copyright 2021-22 Confio GmbH 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/README.md: -------------------------------------------------------------------------------- 1 | # CW20 Bonding curve 2 | 3 | This builds on the [Basic CW20 interface](../../packages/cw20/README.md) 4 | as implemented in [`cw20-base`](../cw20-base/README.md). 5 | 6 | This serves three purposes: 7 | 8 | * A usable and extensible contract for arbitrary bonding curves 9 | * A demonstration of how to extend `cw20-base` to add extra functionality 10 | * A demonstration of the [Receiver interface]([Basic CW20 interface](../../packages/cw20/README.md#receiver)) 11 | 12 | ## Design 13 | 14 | There are two variants - accepting native tokens and accepting cw20 tokens 15 | as the *reserve* token (this is the token that is input to the bonding curve). 16 | 17 | Minting: When the input is sent to the contract (either via `ExecuteMsg::Buy{}` 18 | with native tokens, or via `ExecuteMsg::Receive{}` with cw20 tokens), 19 | those tokens remain on the contract and it issues it's own token to the 20 | sender's account (known as *supply* token). 21 | 22 | Burning: We override the burn function to not only burn the requested tokens, 23 | but also release a proper number of the input tokens to the account that burnt 24 | the custom token 25 | 26 | Curves: `handle` specifies a bonding function, which is sent to parameterize 27 | `handle_fn` (which does all the work). The curve is set when compiling 28 | the contract. In fact many contracts can just wrap `cw20-bonding` and 29 | specify the custom curve parameter. 30 | 31 | Read more about [bonding curve math here](https://yos.io/2018/11/10/bonding-curves/) 32 | 33 | Note: the first version only accepts native tokens as the 34 | 35 | ### Math 36 | 37 | Given a price curve `f(x)` = price of the `x`th token, we want to figure out 38 | how to buy into and sell from the bonding curve. In fact we can look at 39 | the total supply issued. let `F(x)` be the integral of `f(x)`. We have issued 40 | `x` tokens for `F(x)` sent to the contract. Or, in reverse, if we send 41 | `x` tokens to the contract, it will mint `F^-1(x)` tokens. 42 | 43 | From this we can create some formulas. Assume we currently have issued `S` 44 | tokens in exchange for `N = F(S)` input tokens. If someone sends us `x` tokens, 45 | how much will we issue? 46 | 47 | `F^-1(N+x) - F^-1(N)` = `F^-1(N+x) - S` 48 | 49 | And if we sell `x` tokens, how much we will get out: 50 | 51 | `F(S) - F(S-x)` = `N - F(S-x)` 52 | 53 | Just one calculation each side. To be safe, make sure to round down and 54 | always check against `F(S)` when using `F^-1(S)` to estimate how much 55 | should be issued. This will also safely give us how many tokens to return. 56 | 57 | There is built in support for safely [raising i128 to an integer power](https://doc.rust-lang.org/std/primitive.i128.html#method.checked_pow). 58 | There is also a crate to [provide nth-root of for all integers](https://docs.rs/num-integer/0.1.43/num_integer/trait.Roots.html). 59 | With these two, we can handle most math except for logs/exponents. 60 | 61 | Compare this to [writing it all in solidity](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7b7ff729b82ea73ea168e495d9c94cb901ae95ce/contracts/math/Power.sol) 62 | 63 | Examples: 64 | 65 | Price Constant: `f(x) = k` and `F(x) = kx` and `F^-1(x) = x/k` 66 | 67 | Price Linear: `f(x) = kx` and `F(x) = kx^2/2` and `F^-1(x) = (2x/k)^(0.5)` 68 | 69 | Price Square Root: `f(x) = x^0.5` and `F(x) = x^1.5/1.5` and `F^-1(x) = (1.5*x)^(2/3)` 70 | 71 | We will only implement these curves to start with, and leave it to others to import this with more complex curves, 72 | such as logarithms. -------------------------------------------------------------------------------- /contracts/cw20-bonding/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw20_bonding::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | query: QueryMsg, 9 | execute: ExecuteMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/src/contract.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "library"))] 2 | use cosmwasm_std::entry_point; 3 | use cosmwasm_std::{ 4 | attr, coins, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, 5 | StdError, StdResult, Uint128, 6 | }; 7 | 8 | use cw2::set_contract_version; 9 | use cw20_base::allowances::{ 10 | deduct_allowance, execute_decrease_allowance, execute_increase_allowance, execute_send_from, 11 | execute_transfer_from, query_allowance, 12 | }; 13 | use cw20_base::contract::{ 14 | execute_burn, execute_mint, execute_send, execute_transfer, query_balance, query_token_info, 15 | }; 16 | use cw20_base::state::{MinterData, TokenInfo, TOKEN_INFO}; 17 | 18 | use crate::curves::DecimalPlaces; 19 | use crate::error::ContractError; 20 | use crate::msg::{CurveFn, CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; 21 | use crate::state::{CurveState, CURVE_STATE, CURVE_TYPE}; 22 | use cw_utils::{must_pay, nonpayable}; 23 | 24 | // version info for migration info 25 | const CONTRACT_NAME: &str = "crates.io:cw20-bonding"; 26 | const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); 27 | 28 | #[cfg_attr(not(feature = "library"), entry_point)] 29 | pub fn instantiate( 30 | deps: DepsMut, 31 | env: Env, 32 | info: MessageInfo, 33 | msg: InstantiateMsg, 34 | ) -> Result { 35 | nonpayable(&info)?; 36 | set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 37 | 38 | // store token info using cw20-base format 39 | let data = TokenInfo { 40 | name: msg.name, 41 | symbol: msg.symbol, 42 | decimals: msg.decimals, 43 | total_supply: Uint128::zero(), 44 | // set self as minter, so we can properly execute mint and burn 45 | mint: Some(MinterData { 46 | minter: env.contract.address, 47 | cap: None, 48 | }), 49 | }; 50 | TOKEN_INFO.save(deps.storage, &data)?; 51 | 52 | let places = DecimalPlaces::new(msg.decimals, msg.reserve_decimals); 53 | let supply = CurveState::new(msg.reserve_denom, places); 54 | CURVE_STATE.save(deps.storage, &supply)?; 55 | 56 | CURVE_TYPE.save(deps.storage, &msg.curve_type)?; 57 | 58 | Ok(Response::default()) 59 | } 60 | 61 | #[cfg_attr(not(feature = "library"), entry_point)] 62 | pub fn execute( 63 | deps: DepsMut, 64 | env: Env, 65 | info: MessageInfo, 66 | msg: ExecuteMsg, 67 | ) -> Result { 68 | // default implementation stores curve info as enum, you can do something else in a derived 69 | // contract and just pass in your custom curve to do_execute 70 | let curve_type = CURVE_TYPE.load(deps.storage)?; 71 | let curve_fn = curve_type.to_curve_fn(); 72 | do_execute(deps, env, info, msg, curve_fn) 73 | } 74 | 75 | /// We pull out logic here, so we can import this from another contract and set a different Curve. 76 | /// This contacts sets a curve with an enum in InstantiateMsg and stored in state, but you may want 77 | /// to use custom math not included - make this easily reusable 78 | pub fn do_execute( 79 | deps: DepsMut, 80 | env: Env, 81 | info: MessageInfo, 82 | msg: ExecuteMsg, 83 | curve_fn: CurveFn, 84 | ) -> Result { 85 | match msg { 86 | ExecuteMsg::Buy {} => execute_buy(deps, env, info, curve_fn), 87 | 88 | // we override these from cw20 89 | ExecuteMsg::Burn { amount } => Ok(execute_sell(deps, env, info, curve_fn, amount)?), 90 | ExecuteMsg::BurnFrom { owner, amount } => { 91 | Ok(execute_sell_from(deps, env, info, curve_fn, owner, amount)?) 92 | } 93 | 94 | // these all come from cw20-base to implement the cw20 standard 95 | ExecuteMsg::Transfer { recipient, amount } => { 96 | Ok(execute_transfer(deps, env, info, recipient, amount)?) 97 | } 98 | ExecuteMsg::Send { 99 | contract, 100 | amount, 101 | msg, 102 | } => Ok(execute_send(deps, env, info, contract, amount, msg)?), 103 | ExecuteMsg::IncreaseAllowance { 104 | spender, 105 | amount, 106 | expires, 107 | } => Ok(execute_increase_allowance( 108 | deps, env, info, spender, amount, expires, 109 | )?), 110 | ExecuteMsg::DecreaseAllowance { 111 | spender, 112 | amount, 113 | expires, 114 | } => Ok(execute_decrease_allowance( 115 | deps, env, info, spender, amount, expires, 116 | )?), 117 | ExecuteMsg::TransferFrom { 118 | owner, 119 | recipient, 120 | amount, 121 | } => Ok(execute_transfer_from( 122 | deps, env, info, owner, recipient, amount, 123 | )?), 124 | ExecuteMsg::SendFrom { 125 | owner, 126 | contract, 127 | amount, 128 | msg, 129 | } => Ok(execute_send_from( 130 | deps, env, info, owner, contract, amount, msg, 131 | )?), 132 | } 133 | } 134 | 135 | pub fn execute_buy( 136 | deps: DepsMut, 137 | env: Env, 138 | info: MessageInfo, 139 | curve_fn: CurveFn, 140 | ) -> Result { 141 | let mut state = CURVE_STATE.load(deps.storage)?; 142 | 143 | let payment = must_pay(&info, &state.reserve_denom)?; 144 | 145 | // calculate how many tokens can be purchased with this and mint them 146 | let curve = curve_fn(state.clone().decimals); 147 | state.reserve += payment; 148 | let new_supply = curve.supply(state.reserve); 149 | let minted = new_supply 150 | .checked_sub(state.supply) 151 | .map_err(StdError::overflow)?; 152 | state.supply = new_supply; 153 | CURVE_STATE.save(deps.storage, &state)?; 154 | 155 | // call into cw20-base to mint the token, call as self as no one else is allowed 156 | let sub_info = MessageInfo { 157 | sender: env.contract.address.clone(), 158 | funds: vec![], 159 | }; 160 | execute_mint(deps, env, sub_info, info.sender.to_string(), minted)?; 161 | 162 | // bond them to the validator 163 | let res = Response::new() 164 | .add_attribute("action", "buy") 165 | .add_attribute("from", info.sender) 166 | .add_attribute("reserve", payment) 167 | .add_attribute("supply", minted); 168 | Ok(res) 169 | } 170 | 171 | pub fn execute_sell( 172 | deps: DepsMut, 173 | env: Env, 174 | info: MessageInfo, 175 | curve_fn: CurveFn, 176 | amount: Uint128, 177 | ) -> Result { 178 | nonpayable(&info)?; 179 | let receiver = info.sender.clone(); 180 | // do all the work 181 | let mut res = do_sell(deps, env, info, curve_fn, receiver, amount)?; 182 | 183 | // add our custom attributes 184 | res.attributes.push(attr("action", "burn")); 185 | Ok(res) 186 | } 187 | 188 | pub fn execute_sell_from( 189 | deps: DepsMut, 190 | env: Env, 191 | info: MessageInfo, 192 | curve_fn: CurveFn, 193 | owner: String, 194 | amount: Uint128, 195 | ) -> Result { 196 | nonpayable(&info)?; 197 | let owner_addr = deps.api.addr_validate(&owner)?; 198 | let spender_addr = info.sender.clone(); 199 | 200 | // deduct allowance before doing anything else have enough allowance 201 | deduct_allowance(deps.storage, &owner_addr, &spender_addr, &env.block, amount)?; 202 | 203 | // do all the work in do_sell 204 | let receiver_addr = info.sender; 205 | let owner_info = MessageInfo { 206 | sender: owner_addr, 207 | funds: info.funds, 208 | }; 209 | let mut res = do_sell( 210 | deps, 211 | env, 212 | owner_info, 213 | curve_fn, 214 | receiver_addr.clone(), 215 | amount, 216 | )?; 217 | 218 | // add our custom attributes 219 | res.attributes.push(attr("action", "burn_from")); 220 | res.attributes.push(attr("by", receiver_addr)); 221 | Ok(res) 222 | } 223 | 224 | fn do_sell( 225 | mut deps: DepsMut, 226 | env: Env, 227 | // info.sender is the one burning tokens 228 | info: MessageInfo, 229 | curve_fn: CurveFn, 230 | // receiver is the one who gains (same for execute_sell, diff for execute_sell_from) 231 | receiver: Addr, 232 | amount: Uint128, 233 | ) -> Result { 234 | // burn from the caller, this ensures there are tokens to cover this 235 | execute_burn(deps.branch(), env, info.clone(), amount)?; 236 | 237 | // calculate how many tokens can be purchased with this and mint them 238 | let mut state = CURVE_STATE.load(deps.storage)?; 239 | let curve = curve_fn(state.clone().decimals); 240 | state.supply = state 241 | .supply 242 | .checked_sub(amount) 243 | .map_err(StdError::overflow)?; 244 | let new_reserve = curve.reserve(state.supply); 245 | let released = state 246 | .reserve 247 | .checked_sub(new_reserve) 248 | .map_err(StdError::overflow)?; 249 | state.reserve = new_reserve; 250 | CURVE_STATE.save(deps.storage, &state)?; 251 | 252 | // now send the tokens to the sender (TODO: for sell_from we do something else, right???) 253 | let msg = BankMsg::Send { 254 | to_address: receiver.to_string(), 255 | amount: coins(released.u128(), state.reserve_denom), 256 | }; 257 | let res = Response::new() 258 | .add_message(msg) 259 | .add_attribute("from", info.sender) 260 | .add_attribute("supply", amount) 261 | .add_attribute("reserve", released); 262 | Ok(res) 263 | } 264 | 265 | #[cfg_attr(not(feature = "library"), entry_point)] 266 | pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { 267 | // default implementation stores curve info as enum, you can do something else in a derived 268 | // contract and just pass in your custom curve to do_execute 269 | let curve_type = CURVE_TYPE.load(deps.storage)?; 270 | let curve_fn = curve_type.to_curve_fn(); 271 | do_query(deps, env, msg, curve_fn) 272 | } 273 | 274 | /// We pull out logic here, so we can import this from another contract and set a different Curve. 275 | /// This contacts sets a curve with an enum in InstantitateMsg and stored in state, but you may want 276 | /// to use custom math not included - make this easily reusable 277 | pub fn do_query(deps: Deps, _env: Env, msg: QueryMsg, curve_fn: CurveFn) -> StdResult { 278 | match msg { 279 | // custom queries 280 | QueryMsg::CurveInfo {} => to_binary(&query_curve_info(deps, curve_fn)?), 281 | // inherited from cw20-base 282 | QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?), 283 | QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?), 284 | QueryMsg::Allowance { owner, spender } => { 285 | to_binary(&query_allowance(deps, owner, spender)?) 286 | } 287 | } 288 | } 289 | 290 | pub fn query_curve_info(deps: Deps, curve_fn: CurveFn) -> StdResult { 291 | let CurveState { 292 | reserve, 293 | supply, 294 | reserve_denom, 295 | decimals, 296 | } = CURVE_STATE.load(deps.storage)?; 297 | 298 | // This we can get from the local digits stored in instantiate 299 | let curve = curve_fn(decimals); 300 | let spot_price = curve.spot_price(supply); 301 | 302 | Ok(CurveInfoResponse { 303 | reserve, 304 | supply, 305 | spot_price, 306 | reserve_denom, 307 | }) 308 | } 309 | 310 | // this is poor mans "skip" flag 311 | #[cfg(test)] 312 | mod tests { 313 | use super::*; 314 | use crate::msg::CurveType; 315 | use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; 316 | use cosmwasm_std::{coin, Decimal, OverflowError, OverflowOperation, StdError, SubMsg}; 317 | use cw_utils::PaymentError; 318 | 319 | const DENOM: &str = "satoshi"; 320 | const CREATOR: &str = "creator"; 321 | const INVESTOR: &str = "investor"; 322 | const BUYER: &str = "buyer"; 323 | 324 | fn default_instantiate( 325 | decimals: u8, 326 | reserve_decimals: u8, 327 | curve_type: CurveType, 328 | ) -> InstantiateMsg { 329 | InstantiateMsg { 330 | name: "Bonded".to_string(), 331 | symbol: "EPOXY".to_string(), 332 | decimals, 333 | reserve_denom: DENOM.to_string(), 334 | reserve_decimals, 335 | curve_type, 336 | } 337 | } 338 | 339 | fn get_balance>(deps: Deps, addr: U) -> Uint128 { 340 | query_balance(deps, addr.into()).unwrap().balance 341 | } 342 | 343 | fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { 344 | // this matches `linear_curve` test case from curves.rs 345 | let creator = String::from(CREATOR); 346 | let msg = default_instantiate(decimals, reserve_decimals, curve_type); 347 | let info = mock_info(&creator, &[]); 348 | 349 | // make sure we can instantiate with this 350 | let res = instantiate(deps, mock_env(), info, msg).unwrap(); 351 | assert_eq!(0, res.messages.len()); 352 | } 353 | 354 | #[test] 355 | fn proper_instantiation() { 356 | let mut deps = mock_dependencies(); 357 | 358 | // this matches `linear_curve` test case from curves.rs 359 | let creator = String::from("creator"); 360 | let curve_type = CurveType::SquareRoot { 361 | slope: Uint128::new(1), 362 | scale: 1, 363 | }; 364 | let msg = default_instantiate(2, 8, curve_type.clone()); 365 | let info = mock_info(&creator, &[]); 366 | 367 | // make sure we can instantiate with this 368 | let res = instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); 369 | assert_eq!(0, res.messages.len()); 370 | 371 | // token info is proper 372 | let token = query_token_info(deps.as_ref()).unwrap(); 373 | assert_eq!(&token.name, &msg.name); 374 | assert_eq!(&token.symbol, &msg.symbol); 375 | assert_eq!(token.decimals, 2); 376 | assert_eq!(token.total_supply, Uint128::zero()); 377 | 378 | // curve state is sensible 379 | let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); 380 | assert_eq!(state.reserve, Uint128::zero()); 381 | assert_eq!(state.supply, Uint128::zero()); 382 | assert_eq!(state.reserve_denom.as_str(), DENOM); 383 | // spot price 0 as supply is 0 384 | assert_eq!(state.spot_price, Decimal::zero()); 385 | 386 | // curve type is stored properly 387 | let curve = CURVE_TYPE.load(&deps.storage).unwrap(); 388 | assert_eq!(curve_type, curve); 389 | 390 | // no balance 391 | assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); 392 | } 393 | 394 | #[test] 395 | fn buy_issues_tokens() { 396 | let mut deps = mock_dependencies(); 397 | let curve_type = CurveType::Linear { 398 | slope: Uint128::new(1), 399 | scale: 1, 400 | }; 401 | setup_test(deps.as_mut(), 2, 8, curve_type.clone()); 402 | 403 | // succeeds with proper token (5 BTC = 5*10^8 satoshi) 404 | let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); 405 | let buy = ExecuteMsg::Buy {}; 406 | execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); 407 | 408 | // bob got 1000 EPOXY (10.00) 409 | assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); 410 | assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); 411 | 412 | // send them all to buyer 413 | let info = mock_info(INVESTOR, &[]); 414 | let send = ExecuteMsg::Transfer { 415 | recipient: BUYER.into(), 416 | amount: Uint128::new(1000), 417 | }; 418 | execute(deps.as_mut(), mock_env(), info, send).unwrap(); 419 | 420 | // ensure balances updated 421 | assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); 422 | assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); 423 | 424 | // second stake needs more to get next 1000 EPOXY 425 | let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); 426 | execute(deps.as_mut(), mock_env(), info, buy).unwrap(); 427 | 428 | // ensure balances updated 429 | assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); 430 | assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); 431 | 432 | // check curve info updated 433 | let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); 434 | assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); 435 | assert_eq!(curve.supply, Uint128::new(2000)); 436 | assert_eq!(curve.spot_price, Decimal::percent(200)); 437 | 438 | // check token info updated 439 | let token = query_token_info(deps.as_ref()).unwrap(); 440 | assert_eq!(token.decimals, 2); 441 | assert_eq!(token.total_supply, Uint128::new(2000)); 442 | } 443 | 444 | #[test] 445 | fn bonding_fails_with_wrong_denom() { 446 | let mut deps = mock_dependencies(); 447 | let curve_type = CurveType::Linear { 448 | slope: Uint128::new(1), 449 | scale: 1, 450 | }; 451 | setup_test(deps.as_mut(), 2, 8, curve_type); 452 | 453 | // fails when no tokens sent 454 | let info = mock_info(INVESTOR, &[]); 455 | let buy = ExecuteMsg::Buy {}; 456 | let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); 457 | assert_eq!(err, PaymentError::NoFunds {}.into()); 458 | 459 | // fails when wrong tokens sent 460 | let info = mock_info(INVESTOR, &coins(1234567, "wei")); 461 | let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); 462 | assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); 463 | 464 | // fails when too many tokens sent 465 | let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); 466 | let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); 467 | assert_eq!(err, PaymentError::MultipleDenoms {}.into()); 468 | } 469 | 470 | #[test] 471 | fn burning_sends_reserve() { 472 | let mut deps = mock_dependencies(); 473 | let curve_type = CurveType::Linear { 474 | slope: Uint128::new(1), 475 | scale: 1, 476 | }; 477 | setup_test(deps.as_mut(), 2, 8, curve_type.clone()); 478 | 479 | // succeeds with proper token (20 BTC = 20*10^8 satoshi) 480 | let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); 481 | let buy = ExecuteMsg::Buy {}; 482 | execute(deps.as_mut(), mock_env(), info, buy).unwrap(); 483 | 484 | // bob got 2000 EPOXY (20.00) 485 | assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); 486 | 487 | // cannot burn too much 488 | let info = mock_info(INVESTOR, &[]); 489 | let burn = ExecuteMsg::Burn { 490 | amount: Uint128::new(3000), 491 | }; 492 | let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); 493 | assert_eq!( 494 | err, 495 | ContractError::Base(cw20_base::ContractError::Std(StdError::overflow( 496 | OverflowError::new(OverflowOperation::Sub, 2000, 3000) 497 | ))) 498 | ); 499 | 500 | // burn 1000 EPOXY to get back 15BTC (*10^8) 501 | let info = mock_info(INVESTOR, &[]); 502 | let burn = ExecuteMsg::Burn { 503 | amount: Uint128::new(1000), 504 | }; 505 | let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); 506 | 507 | // balance is lower 508 | assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); 509 | 510 | // ensure we got our money back 511 | assert_eq!(1, res.messages.len()); 512 | assert_eq!( 513 | &res.messages[0], 514 | &SubMsg::new(BankMsg::Send { 515 | to_address: INVESTOR.into(), 516 | amount: coins(1_500_000_000, DENOM), 517 | }) 518 | ); 519 | 520 | // check curve info updated 521 | let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); 522 | assert_eq!(curve.reserve, Uint128::new(500_000_000)); 523 | assert_eq!(curve.supply, Uint128::new(1000)); 524 | assert_eq!(curve.spot_price, Decimal::percent(100)); 525 | 526 | // check token info updated 527 | let token = query_token_info(deps.as_ref()).unwrap(); 528 | assert_eq!(token.decimals, 2); 529 | assert_eq!(token.total_supply, Uint128::new(1000)); 530 | } 531 | 532 | #[test] 533 | fn cw20_imports_work() { 534 | let mut deps = mock_dependencies(); 535 | let curve_type = CurveType::Constant { 536 | value: Uint128::new(15), 537 | scale: 1, 538 | }; 539 | setup_test(deps.as_mut(), 9, 6, curve_type); 540 | 541 | let alice: &str = "alice"; 542 | let bob: &str = "bobby"; 543 | let carl: &str = "carl"; 544 | 545 | // spend 45_000 uatom for 30_000_000 EPOXY 546 | let info = mock_info(bob, &coins(45_000, DENOM)); 547 | let buy = ExecuteMsg::Buy {}; 548 | execute(deps.as_mut(), mock_env(), info, buy).unwrap(); 549 | 550 | // check balances 551 | assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(30_000_000)); 552 | assert_eq!(get_balance(deps.as_ref(), carl), Uint128::zero()); 553 | 554 | // send coins to carl 555 | let bob_info = mock_info(bob, &[]); 556 | let transfer = ExecuteMsg::Transfer { 557 | recipient: carl.into(), 558 | amount: Uint128::new(2_000_000), 559 | }; 560 | execute(deps.as_mut(), mock_env(), bob_info.clone(), transfer).unwrap(); 561 | assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(28_000_000)); 562 | assert_eq!(get_balance(deps.as_ref(), carl), Uint128::new(2_000_000)); 563 | 564 | // allow alice 565 | let allow = ExecuteMsg::IncreaseAllowance { 566 | spender: alice.into(), 567 | amount: Uint128::new(35_000_000), 568 | expires: None, 569 | }; 570 | execute(deps.as_mut(), mock_env(), bob_info, allow).unwrap(); 571 | assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(28_000_000)); 572 | assert_eq!(get_balance(deps.as_ref(), alice), Uint128::zero()); 573 | assert_eq!( 574 | query_allowance(deps.as_ref(), bob.into(), alice.into()) 575 | .unwrap() 576 | .allowance, 577 | Uint128::new(35_000_000) 578 | ); 579 | 580 | // alice takes some for herself 581 | let self_pay = ExecuteMsg::TransferFrom { 582 | owner: bob.into(), 583 | recipient: alice.into(), 584 | amount: Uint128::new(25_000_000), 585 | }; 586 | let alice_info = mock_info(alice, &[]); 587 | execute(deps.as_mut(), mock_env(), alice_info, self_pay).unwrap(); 588 | assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(3_000_000)); 589 | assert_eq!(get_balance(deps.as_ref(), alice), Uint128::new(25_000_000)); 590 | assert_eq!(get_balance(deps.as_ref(), carl), Uint128::new(2_000_000)); 591 | assert_eq!( 592 | query_allowance(deps.as_ref(), bob.into(), alice.into()) 593 | .unwrap() 594 | .allowance, 595 | Uint128::new(10_000_000) 596 | ); 597 | 598 | // test burn from works properly (burn tested in burning_sends_reserve) 599 | // cannot burn more than they have 600 | 601 | let info = mock_info(alice, &[]); 602 | let burn_from = ExecuteMsg::BurnFrom { 603 | owner: bob.into(), 604 | amount: Uint128::new(3_300_000), 605 | }; 606 | let err = execute(deps.as_mut(), mock_env(), info, burn_from).unwrap_err(); 607 | assert_eq!( 608 | err, 609 | ContractError::Base(cw20_base::ContractError::Std(StdError::overflow( 610 | OverflowError::new(OverflowOperation::Sub, 3000000, 3300000) 611 | ))) 612 | ); 613 | 614 | // burn 1_000_000 EPOXY to get back 1_500 DENOM (constant curve) 615 | let info = mock_info(alice, &[]); 616 | let burn_from = ExecuteMsg::BurnFrom { 617 | owner: bob.into(), 618 | amount: Uint128::new(1_000_000), 619 | }; 620 | let res = execute(deps.as_mut(), mock_env(), info, burn_from).unwrap(); 621 | 622 | // bob balance is lower, not alice 623 | assert_eq!(get_balance(deps.as_ref(), alice), Uint128::new(25_000_000)); 624 | assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(2_000_000)); 625 | 626 | // ensure alice got our money back 627 | assert_eq!(1, res.messages.len()); 628 | assert_eq!( 629 | &res.messages[0], 630 | &SubMsg::new(BankMsg::Send { 631 | to_address: alice.into(), 632 | amount: coins(1_500, DENOM), 633 | }) 634 | ); 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/src/curves.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use integer_cbrt::IntegerCubeRoot; 3 | use integer_sqrt::IntegerSquareRoot; 4 | use rust_decimal::prelude::ToPrimitive; 5 | use rust_decimal::Decimal; 6 | use std::str::FromStr; 7 | 8 | use cosmwasm_std::{Decimal as StdDecimal, Uint128}; 9 | 10 | /// This defines the curves we are using. 11 | /// 12 | /// I am struggling on what type to use for the math. Tokens are often stored as Uint128, 13 | /// but they may have 6 or 9 digits. When using constant or linear functions, this doesn't matter 14 | /// much, but for non-linear functions a lot more. Also, supply and reserve most likely have different 15 | /// decimals... either we leave it for the callers to normalize and accept a `Decimal` input, 16 | /// or we pass in `Uint128` as well as the decimal places for supply and reserve. 17 | /// 18 | /// After working the first route and realizing that `Decimal` is not all that great to work with 19 | /// when you want to do more complex math than add and multiply `Uint128`, I decided to go the second 20 | /// route. That made the signatures quite complex and my final idea was to pass in `supply_decimal` 21 | /// and `reserve_decimal` in the curve constructors. 22 | pub trait Curve { 23 | /// Returns the spot price given the supply. 24 | /// `f(x)` from the README 25 | fn spot_price(&self, supply: Uint128) -> StdDecimal; 26 | 27 | /// Returns the total price paid up to purchase supply tokens (integral) 28 | /// `F(x)` from the README 29 | fn reserve(&self, supply: Uint128) -> Uint128; 30 | 31 | /// Inverse of reserve. Returns how many tokens would be issued 32 | /// with a total paid amount of reserve. 33 | /// `F^-1(x)` from the README 34 | fn supply(&self, reserve: Uint128) -> Uint128; 35 | } 36 | 37 | /// decimal returns an object = num * 10 ^ -scale 38 | /// We use this function in contract.rs rather than call the crate constructor 39 | /// itself, in case we want to swap out the implementation, we can do it only in this file. 40 | pub fn decimal>(num: T, scale: u32) -> Decimal { 41 | Decimal::from_i128_with_scale(num.into() as i128, scale) 42 | } 43 | 44 | /// StdDecimal stores as a u128 with 18 decimal points of precision 45 | fn decimal_to_std(x: Decimal) -> StdDecimal { 46 | // this seems straight-forward (if inefficient), converting via string representation 47 | // TODO: execute errors better? Result? 48 | StdDecimal::from_str(&x.to_string()).unwrap() 49 | 50 | // // maybe a better approach doing math, not sure about rounding 51 | // 52 | // // try to preserve decimal points, max 9 53 | // let digits = min(x.scale(), 9); 54 | // let multiplier = 10u128.pow(digits); 55 | // 56 | // // we multiply up before we round off to u128, 57 | // // let StdDecimal do its best to keep these decimal places 58 | // let nominator = (x * decimal(multiplier, 0)).to_u128().unwrap(); 59 | // StdDecimal::from_ratio(nominator, multiplier) 60 | } 61 | 62 | /// spot price is always a constant value 63 | pub struct Constant { 64 | pub value: Decimal, 65 | pub normalize: DecimalPlaces, 66 | } 67 | 68 | impl Constant { 69 | pub fn new(value: Decimal, normalize: DecimalPlaces) -> Self { 70 | Self { value, normalize } 71 | } 72 | } 73 | 74 | impl Curve for Constant { 75 | // we need to normalize value with the reserve decimal places 76 | // (eg 0.1 value would return 100_000 if reserve was uatom) 77 | fn spot_price(&self, _supply: Uint128) -> StdDecimal { 78 | // f(x) = self.value 79 | decimal_to_std(self.value) 80 | } 81 | 82 | /// Returns total number of reserve tokens needed to purchase a given number of supply tokens. 83 | /// Note that both need to be normalized. 84 | fn reserve(&self, supply: Uint128) -> Uint128 { 85 | // f(x) = supply * self.value 86 | let reserve = self.normalize.from_supply(supply) * self.value; 87 | self.normalize.clone().to_reserve(reserve) 88 | } 89 | 90 | fn supply(&self, reserve: Uint128) -> Uint128 { 91 | // f(x) = reserve / self.value 92 | let supply = self.normalize.from_reserve(reserve) / self.value; 93 | self.normalize.clone().to_supply(supply) 94 | } 95 | } 96 | 97 | /// spot_price is slope * supply 98 | pub struct Linear { 99 | pub slope: Decimal, 100 | pub normalize: DecimalPlaces, 101 | } 102 | 103 | impl Linear { 104 | pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { 105 | Self { slope, normalize } 106 | } 107 | } 108 | 109 | impl Curve for Linear { 110 | fn spot_price(&self, supply: Uint128) -> StdDecimal { 111 | // f(x) = supply * self.value 112 | let out = self.normalize.from_supply(supply) * self.slope; 113 | decimal_to_std(out) 114 | } 115 | 116 | fn reserve(&self, supply: Uint128) -> Uint128 { 117 | // f(x) = self.slope * supply * supply / 2 118 | let normalized = self.normalize.from_supply(supply); 119 | let square = normalized * normalized; 120 | // Note: multiplying by 0.5 is much faster than dividing by 2 121 | let reserve = square * self.slope * Decimal::new(5, 1); 122 | self.normalize.clone().to_reserve(reserve) 123 | } 124 | 125 | fn supply(&self, reserve: Uint128) -> Uint128 { 126 | // f(x) = (2 * reserve / self.slope) ^ 0.5 127 | // note: use addition here to optimize 2* operation 128 | let square = self.normalize.from_reserve(reserve + reserve) / self.slope; 129 | let supply = square_root(square); 130 | self.normalize.clone().to_supply(supply) 131 | } 132 | } 133 | 134 | /// spot_price is slope * (supply)^0.5 135 | pub struct SquareRoot { 136 | pub slope: Decimal, 137 | pub normalize: DecimalPlaces, 138 | } 139 | 140 | impl SquareRoot { 141 | pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { 142 | Self { slope, normalize } 143 | } 144 | } 145 | 146 | impl Curve for SquareRoot { 147 | fn spot_price(&self, supply: Uint128) -> StdDecimal { 148 | // f(x) = self.slope * supply^0.5 149 | let square = self.normalize.from_supply(supply); 150 | let root = square_root(square); 151 | decimal_to_std(root * self.slope) 152 | } 153 | 154 | fn reserve(&self, supply: Uint128) -> Uint128 { 155 | // f(x) = self.slope * supply * supply^0.5 / 1.5 156 | let normalized = self.normalize.from_supply(supply); 157 | let root = square_root(normalized); 158 | let reserve = self.slope * normalized * root / Decimal::new(15, 1); 159 | self.normalize.clone().to_reserve(reserve) 160 | } 161 | 162 | fn supply(&self, reserve: Uint128) -> Uint128 { 163 | // f(x) = (1.5 * reserve / self.slope) ^ (2/3) 164 | let base = self.normalize.from_reserve(reserve) * Decimal::new(15, 1) / self.slope; 165 | let squared = base * base; 166 | let supply = cube_root(squared); 167 | self.normalize.clone().to_supply(supply) 168 | } 169 | } 170 | 171 | // we multiply by 10^18, turn to int, take square root, then divide by 10^9 as we convert back to decimal 172 | fn square_root(square: Decimal) -> Decimal { 173 | // must be even 174 | // TODO: this can overflow easily at 18... what is a good value? 175 | const EXTRA_DIGITS: u32 = 12; 176 | let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); 177 | 178 | // multiply by 10^18 and turn to u128 179 | let extended = square * decimal(multiplier, 0); 180 | let extended = extended.floor().to_u128().unwrap(); 181 | 182 | // take square root, and build a decimal again 183 | let root = extended.integer_sqrt(); 184 | decimal(root, EXTRA_DIGITS / 2) 185 | } 186 | 187 | // we multiply by 10^9, turn to int, take cube root, then divide by 10^3 as we convert back to decimal 188 | fn cube_root(cube: Decimal) -> Decimal { 189 | // must be multiple of 3 190 | // TODO: what is a good value? 191 | const EXTRA_DIGITS: u32 = 9; 192 | let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); 193 | 194 | // multiply out and turn to u128 195 | let extended = cube * decimal(multiplier, 0); 196 | let extended = extended.floor().to_u128().unwrap(); 197 | 198 | // take cube root, and build a decimal again 199 | let root = extended.integer_cbrt(); 200 | decimal(root, EXTRA_DIGITS / 3) 201 | } 202 | 203 | /// DecimalPlaces should be passed into curve constructors 204 | #[cw_serde] 205 | pub struct DecimalPlaces { 206 | /// Number of decimal places for the supply token (this is what was passed in cw20-base instantiate 207 | pub supply: u32, 208 | /// Number of decimal places for the reserve token (eg. 6 for uatom, 9 for nstep, 18 for wei) 209 | pub reserve: u32, 210 | } 211 | 212 | impl DecimalPlaces { 213 | pub fn new(supply: u8, reserve: u8) -> Self { 214 | DecimalPlaces { 215 | supply: supply as u32, 216 | reserve: reserve as u32, 217 | } 218 | } 219 | 220 | pub fn to_reserve(self, reserve: Decimal) -> Uint128 { 221 | let factor = decimal(10u128.pow(self.reserve), 0); 222 | let out = reserve * factor; 223 | // TODO: execute overflow better? Result? 224 | out.floor().to_u128().unwrap().into() 225 | } 226 | 227 | pub fn to_supply(self, supply: Decimal) -> Uint128 { 228 | let factor = decimal(10u128.pow(self.supply), 0); 229 | let out = supply * factor; 230 | // TODO: execute overflow better? Result? 231 | out.floor().to_u128().unwrap().into() 232 | } 233 | 234 | pub fn from_supply(&self, supply: Uint128) -> Decimal { 235 | decimal(supply, self.supply) 236 | } 237 | 238 | pub fn from_reserve(&self, reserve: Uint128) -> Decimal { 239 | decimal(reserve, self.reserve) 240 | } 241 | } 242 | 243 | #[cfg(test)] 244 | mod tests { 245 | use super::*; 246 | // TODO: test DecimalPlaces return proper decimals 247 | 248 | #[test] 249 | fn constant_curve() { 250 | // supply is nstep (9), reserve is uatom (6) 251 | let normalize = DecimalPlaces::new(9, 6); 252 | let curve = Constant::new(decimal(15u128, 1), normalize); 253 | 254 | // do some sanity checks.... 255 | // spot price is always 1.5 ATOM 256 | assert_eq!( 257 | StdDecimal::percent(150), 258 | curve.spot_price(Uint128::new(123)) 259 | ); 260 | 261 | // if we have 30 STEP, we should have 45 ATOM 262 | let reserve = curve.reserve(Uint128::new(30_000_000_000)); 263 | assert_eq!(Uint128::new(45_000_000), reserve); 264 | 265 | // if we have 36 ATOM, we should have 24 STEP 266 | let supply = curve.supply(Uint128::new(36_000_000)); 267 | assert_eq!(Uint128::new(24_000_000_000), supply); 268 | } 269 | 270 | #[test] 271 | fn linear_curve() { 272 | // supply is usdt (2), reserve is btc (8) 273 | let normalize = DecimalPlaces::new(2, 8); 274 | // slope is 0.1 (eg hits 1.0 after 10btc) 275 | let curve = Linear::new(decimal(1u128, 1), normalize); 276 | 277 | // do some sanity checks.... 278 | // spot price is 0.1 with 1 USDT supply 279 | assert_eq!( 280 | StdDecimal::permille(100), 281 | curve.spot_price(Uint128::new(100)) 282 | ); 283 | // spot price is 1.7 with 17 USDT supply 284 | assert_eq!( 285 | StdDecimal::permille(1700), 286 | curve.spot_price(Uint128::new(1700)) 287 | ); 288 | // spot price is 0.212 with 2.12 USDT supply 289 | assert_eq!( 290 | StdDecimal::permille(212), 291 | curve.spot_price(Uint128::new(212)) 292 | ); 293 | 294 | // if we have 10 USDT, we should have 5 BTC 295 | let reserve = curve.reserve(Uint128::new(1000)); 296 | assert_eq!(Uint128::new(500_000_000), reserve); 297 | // if we have 20 USDT, we should have 20 BTC 298 | let reserve = curve.reserve(Uint128::new(2000)); 299 | assert_eq!(Uint128::new(2_000_000_000), reserve); 300 | 301 | // if we have 1.25 BTC, we should have 5 USDT 302 | let supply = curve.supply(Uint128::new(125_000_000)); 303 | assert_eq!(Uint128::new(500), supply); 304 | // test square root rounding 305 | // TODO: test when supply has many more decimal places than reserve 306 | // if we have 1.11 BTC, we should have 4.7116875957... USDT 307 | let supply = curve.supply(Uint128::new(111_000_000)); 308 | assert_eq!(Uint128::new(471), supply); 309 | } 310 | 311 | #[test] 312 | fn sqrt_curve() { 313 | // supply is utree (6) reserve is chf (2) 314 | let normalize = DecimalPlaces::new(6, 2); 315 | // slope is 0.35 (eg hits 0.35 after 1 chf, 3.5 after 100chf) 316 | let curve = SquareRoot::new(decimal(35u128, 2), normalize); 317 | 318 | // do some sanity checks.... 319 | // spot price is 0.35 with 1 TREE supply 320 | assert_eq!( 321 | StdDecimal::percent(35), 322 | curve.spot_price(Uint128::new(1_000_000)) 323 | ); 324 | // spot price is 3.5 with 100 TREE supply 325 | assert_eq!( 326 | StdDecimal::percent(350), 327 | curve.spot_price(Uint128::new(100_000_000)) 328 | ); 329 | // spot price should be 23.478713763747788 with 4500 TREE supply (test rounding and reporting here) 330 | // rounds off around 8-9 sig figs (note diff for last points) 331 | assert_eq!( 332 | StdDecimal::from_ratio(2347871365u128, 100_000_000u128), 333 | curve.spot_price(Uint128::new(4_500_000_000)) 334 | ); 335 | 336 | // if we have 1 TREE, we should have 0.2333333333333 CHF 337 | let reserve = curve.reserve(Uint128::new(1_000_000)); 338 | assert_eq!(Uint128::new(23), reserve); 339 | // if we have 100 TREE, we should have 233.333333333 CHF 340 | let reserve = curve.reserve(Uint128::new(100_000_000)); 341 | assert_eq!(Uint128::new(23_333), reserve); 342 | // test rounding 343 | // if we have 235 TREE, we should have 840.5790828021146 CHF 344 | let reserve = curve.reserve(Uint128::new(235_000_000)); 345 | assert_eq!(Uint128::new(84_057), reserve); // round down 346 | 347 | // // if we have 0.23 CHF, we should have 0.990453 TREE (round down) 348 | let supply = curve.supply(Uint128::new(23)); 349 | assert_eq!(Uint128::new(990_000), supply); 350 | // if we have 840.58 CHF, we should have 235.000170 TREE (round down) 351 | let supply = curve.supply(Uint128::new(84058)); 352 | assert_eq!(Uint128::new(235_000_000), supply); 353 | } 354 | 355 | // Idea: generic test that curve.supply(curve.reserve(supply)) == supply (or within some small rounding margin) 356 | } 357 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use cw_utils::PaymentError; 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug, PartialEq)] 6 | pub enum ContractError { 7 | #[error("{0}")] 8 | Std(#[from] StdError), 9 | 10 | #[error("{0}")] 11 | Base(#[from] cw20_base::ContractError), 12 | 13 | #[error("{0}")] 14 | Payment(#[from] PaymentError), 15 | 16 | #[error("Unauthorized")] 17 | Unauthorized {}, 18 | } 19 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | pub mod curves; 3 | mod error; 4 | pub mod msg; 5 | pub mod state; 6 | 7 | pub use crate::error::ContractError; 8 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | 3 | use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; 4 | use cosmwasm_std::{Binary, Decimal, Uint128}; 5 | use cw20::AllowanceResponse as Cw20AllowanceResponse; 6 | use cw20::BalanceResponse as Cw20BalanceResponse; 7 | use cw20::Expiration; 8 | use cw20::TokenInfoResponse as Cw20TokenInfoResponse; 9 | 10 | #[cw_serde] 11 | pub struct InstantiateMsg { 12 | /// name of the supply token 13 | pub name: String, 14 | /// symbol / ticker of the supply token 15 | pub symbol: String, 16 | /// number of decimal places of the supply token, needed for proper curve math. 17 | /// If it is eg. BTC, where a balance of 10^8 means 1 BTC, then use 8 here. 18 | pub decimals: u8, 19 | 20 | /// this is the reserve token denom (only support native for now) 21 | pub reserve_denom: String, 22 | /// number of decimal places for the reserve token, needed for proper curve math. 23 | /// Same format as decimals above, eg. if it is uatom, where 1 unit is 10^-6 ATOM, use 6 here 24 | pub reserve_decimals: u8, 25 | 26 | /// enum to store the curve parameters used for this contract 27 | /// if you want to add a custom Curve, you should make a new contract that imports this one. 28 | /// write a custom `instantiate`, and then dispatch `your::execute` -> `cw20_bonding::do_execute` 29 | /// with your custom curve as a parameter (and same with `query` -> `do_query`) 30 | pub curve_type: CurveType, 31 | } 32 | 33 | pub type CurveFn = Box Box>; 34 | 35 | #[cw_serde] 36 | pub enum CurveType { 37 | /// Constant always returns `value * 10^-scale` as spot price 38 | Constant { value: Uint128, scale: u32 }, 39 | /// Linear returns `slope * 10^-scale * supply` as spot price 40 | Linear { slope: Uint128, scale: u32 }, 41 | /// SquareRoot returns `slope * 10^-scale * supply^0.5` as spot price 42 | SquareRoot { slope: Uint128, scale: u32 }, 43 | } 44 | 45 | impl CurveType { 46 | pub fn to_curve_fn(&self) -> CurveFn { 47 | match self.clone() { 48 | CurveType::Constant { value, scale } => { 49 | let calc = move |places| -> Box { 50 | Box::new(Constant::new(decimal(value, scale), places)) 51 | }; 52 | Box::new(calc) 53 | } 54 | CurveType::Linear { slope, scale } => { 55 | let calc = move |places| -> Box { 56 | Box::new(Linear::new(decimal(slope, scale), places)) 57 | }; 58 | Box::new(calc) 59 | } 60 | CurveType::SquareRoot { slope, scale } => { 61 | let calc = move |places| -> Box { 62 | Box::new(SquareRoot::new(decimal(slope, scale), places)) 63 | }; 64 | Box::new(calc) 65 | } 66 | } 67 | } 68 | } 69 | 70 | #[cw_serde] 71 | pub enum ExecuteMsg { 72 | /// Buy will attempt to purchase as many supply tokens as possible. 73 | /// You must send only reserve tokens in that message 74 | Buy {}, 75 | 76 | /// Implements CW20. Transfer is a base message to move tokens to another account without triggering actions 77 | Transfer { recipient: String, amount: Uint128 }, 78 | /// Implements CW20. Burn is a base message to destroy tokens forever 79 | Burn { amount: Uint128 }, 80 | /// Implements CW20. Send is a base message to transfer tokens to a contract and trigger an action 81 | /// on the receiving contract. 82 | Send { 83 | contract: String, 84 | amount: Uint128, 85 | msg: Binary, 86 | }, 87 | /// Implements CW20 "approval" extension. Allows spender to access an additional amount tokens 88 | /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance 89 | /// expiration with this one. 90 | IncreaseAllowance { 91 | spender: String, 92 | amount: Uint128, 93 | expires: Option, 94 | }, 95 | /// Implements CW20 "approval" extension. Lowers the spender's access of tokens 96 | /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current 97 | /// allowance expiration with this one. 98 | DecreaseAllowance { 99 | spender: String, 100 | amount: Uint128, 101 | expires: Option, 102 | }, 103 | /// Implements CW20 "approval" extension. Transfers amount tokens from owner -> recipient 104 | /// if `env.sender` has sufficient pre-approval. 105 | TransferFrom { 106 | owner: String, 107 | recipient: String, 108 | amount: Uint128, 109 | }, 110 | /// Implements CW20 "approval" extension. Sends amount tokens from owner -> contract 111 | /// if `env.sender` has sufficient pre-approval. 112 | SendFrom { 113 | owner: String, 114 | contract: String, 115 | amount: Uint128, 116 | msg: Binary, 117 | }, 118 | /// Implements CW20 "approval" extension. Destroys tokens forever 119 | BurnFrom { owner: String, amount: Uint128 }, 120 | } 121 | 122 | #[cw_serde] 123 | #[derive(QueryResponses)] 124 | pub enum QueryMsg { 125 | /// Returns the reserve and supply quantities, as well as the spot price to buy 1 token 126 | #[returns(CurveInfoResponse)] 127 | CurveInfo {}, 128 | /// Implements CW20. Returns the current balance of the given address, 0 if unset. 129 | #[returns(Cw20BalanceResponse)] 130 | Balance { address: String }, 131 | /// Implements CW20. Returns metadata on the contract - name, decimals, supply, etc. 132 | #[returns(Cw20TokenInfoResponse)] 133 | TokenInfo {}, 134 | /// Implements CW20 "allowance" extension. 135 | /// Returns how much spender can use from owner account, 0 if unset. 136 | #[returns(Cw20AllowanceResponse)] 137 | Allowance { owner: String, spender: String }, 138 | } 139 | 140 | #[cw_serde] 141 | pub struct CurveInfoResponse { 142 | // how many reserve tokens have been received 143 | pub reserve: Uint128, 144 | // how many supply tokens have been issued 145 | pub supply: Uint128, 146 | pub spot_price: Decimal, 147 | pub reserve_denom: String, 148 | } 149 | -------------------------------------------------------------------------------- /contracts/cw20-bonding/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | 3 | use cosmwasm_std::Uint128; 4 | use cw_storage_plus::Item; 5 | 6 | use crate::curves::DecimalPlaces; 7 | use crate::msg::CurveType; 8 | 9 | /// Supply is dynamic and tracks the current supply of staked and ERC20 tokens. 10 | #[cw_serde] 11 | pub struct CurveState { 12 | /// reserve is how many native tokens exist bonded to the validator 13 | pub reserve: Uint128, 14 | /// supply is how many tokens this contract has issued 15 | pub supply: Uint128, 16 | 17 | // the denom of the reserve token 18 | pub reserve_denom: String, 19 | 20 | // how to normalize reserve and supply 21 | pub decimals: DecimalPlaces, 22 | } 23 | 24 | impl CurveState { 25 | pub fn new(reserve_denom: String, decimals: DecimalPlaces) -> Self { 26 | CurveState { 27 | reserve: Uint128::zero(), 28 | supply: Uint128::zero(), 29 | reserve_denom, 30 | decimals, 31 | } 32 | } 33 | } 34 | 35 | pub const CURVE_STATE: Item = Item::new("curve_state"); 36 | 37 | pub const CURVE_TYPE: Item = Item::new("curve_type"); 38 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --example schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-escrow" 3 | version = "0.14.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Implementation of an escrow that accepts CosmWasm-20 tokens as well as native tokens" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-tokens" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | backtraces = ["cosmwasm-std/backtraces"] 17 | # use library feature to disable all instantiate/execute/query exports 18 | library = [] 19 | 20 | [dependencies] 21 | cw-utils = "0.16.0" 22 | cw2 = "0.16.0" 23 | cw20 = "0.16.0" 24 | cosmwasm-std = "1.1.5" 25 | cw-storage-plus = "0.16.0" 26 | cosmwasm-schema = "1.1.5" 27 | thiserror = "1.0.31" 28 | 29 | [dev-dependencies] 30 | cw-multi-test = "0.16.0" 31 | cw20-base = { version = "0.16.0", features = ["library"] } 32 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/NOTICE: -------------------------------------------------------------------------------- 1 | CW20-Escrow: A CosmWasm escrow contract that handles native and cw20 tokens 2 | Copyright (C) 2020-21 Confio OÜ 3 | Copyright (C) 2021-22 Confio GmbH 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/README.md: -------------------------------------------------------------------------------- 1 | # CW20 Escrow 2 | 3 | This is an escrow meta-contract that allows multiple users to 4 | create independent escrows. Each escrow has a sender, recipient, 5 | and arbiter. It also has a unique id (for future calls to reference it) 6 | and an optional timeout. 7 | 8 | The basic function is the sender creates an escrow with funds. 9 | The arbiter may at any time decide to release the funds to either 10 | the intended recipient or the original sender (but no one else), 11 | and if it passes with optional timeout, anyone can refund the locked 12 | tokens to the original sender. 13 | 14 | We also add a function called "top_up", which allows anyone to add more 15 | funds to the contract at any time. 16 | 17 | ## Token types 18 | 19 | This contract is meant not just to be functional, but also to work as a simple 20 | example of an CW20 "Receiver". And demonstrate how the same calls can be fed 21 | native tokens (via typical `ExecuteMsg` route), or cw20 tokens (via `Receiver` interface). 22 | 23 | Both `create` and `top_up` can be called directly (with a payload of native tokens), 24 | or from a cw20 contract using the [Receiver Interface](../../packages/cw20/README.md#receiver). 25 | This means we can load the escrow with any number of native or cw20 tokens (or a mix), 26 | allow of which get released when the arbiter decides. 27 | 28 | ## Running this contract 29 | 30 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 31 | 32 | You can run unit tests on this via: 33 | 34 | `cargo test` 35 | 36 | Once you are happy with the content, you can compile it to wasm via: 37 | 38 | ``` 39 | RUSTFLAGS='-C link-arg=-s' cargo wasm 40 | cp ../../target/wasm32-unknown-unknown/release/cw20_escrow.wasm . 41 | ls -l cw20_escrow.wasm 42 | sha256sum cw20_escrow.wasm 43 | ``` 44 | 45 | Or for a production-ready (optimized) build, run a build command in the 46 | the repository root: https://github.com/CosmWasm/cw-plus#compiling. 47 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw20_escrow::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | query: QueryMsg, 9 | execute: ExecuteMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, PartialEq)] 5 | pub enum ContractError { 6 | #[error("{0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("Unauthorized")] 10 | Unauthorized {}, 11 | 12 | #[error("Only accepts tokens in the cw20_whitelist")] 13 | NotInWhitelist {}, 14 | 15 | #[error("Escrow is expired")] 16 | Expired {}, 17 | 18 | #[error("Send some coins to create an escrow")] 19 | EmptyBalance {}, 20 | 21 | #[error("Escrow id already in use")] 22 | AlreadyInUse {}, 23 | 24 | #[error("Recipient is not set")] 25 | RecipientNotSet {}, 26 | } 27 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/src/integration_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use cosmwasm_std::{coins, to_binary, Addr, Empty, Uint128}; 4 | use cw20::{Cw20Coin, Cw20Contract, Cw20ExecuteMsg}; 5 | use cw_multi_test::{App, Contract, ContractWrapper, Executor}; 6 | 7 | use crate::msg::{CreateMsg, DetailsResponse, ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg}; 8 | 9 | pub fn contract_escrow() -> Box> { 10 | let contract = ContractWrapper::new( 11 | crate::contract::execute, 12 | crate::contract::instantiate, 13 | crate::contract::query, 14 | ); 15 | Box::new(contract) 16 | } 17 | 18 | pub fn contract_cw20() -> Box> { 19 | let contract = ContractWrapper::new( 20 | cw20_base::contract::execute, 21 | cw20_base::contract::instantiate, 22 | cw20_base::contract::query, 23 | ); 24 | Box::new(contract) 25 | } 26 | 27 | #[test] 28 | // receive cw20 tokens and release upon approval 29 | fn escrow_happy_path_cw20_tokens() { 30 | // set personal balance 31 | let owner = Addr::unchecked("owner"); 32 | let init_funds = coins(2000, "btc"); 33 | 34 | let mut router = App::new(|router, _, storage| { 35 | router 36 | .bank 37 | .init_balance(storage, &owner, init_funds) 38 | .unwrap(); 39 | }); 40 | 41 | // set up cw20 contract with some tokens 42 | let cw20_id = router.store_code(contract_cw20()); 43 | let msg = cw20_base::msg::InstantiateMsg { 44 | name: "Cash Money".to_string(), 45 | symbol: "CASH".to_string(), 46 | decimals: 2, 47 | initial_balances: vec![Cw20Coin { 48 | address: owner.to_string(), 49 | amount: Uint128::new(5000), 50 | }], 51 | mint: None, 52 | marketing: None, 53 | }; 54 | let cash_addr = router 55 | .instantiate_contract(cw20_id, owner.clone(), &msg, &[], "CASH", None) 56 | .unwrap(); 57 | 58 | // set up reflect contract 59 | let escrow_id = router.store_code(contract_escrow()); 60 | let escrow_addr = router 61 | .instantiate_contract( 62 | escrow_id, 63 | owner.clone(), 64 | &InstantiateMsg {}, 65 | &[], 66 | "Escrow", 67 | None, 68 | ) 69 | .unwrap(); 70 | 71 | // they are different 72 | assert_ne!(cash_addr, escrow_addr); 73 | 74 | // set up cw20 helpers 75 | let cash = Cw20Contract(cash_addr.clone()); 76 | 77 | // ensure our balances 78 | let owner_balance = cash.balance::<_, _, Empty>(&router, owner.clone()).unwrap(); 79 | assert_eq!(owner_balance, Uint128::new(5000)); 80 | let escrow_balance = cash 81 | .balance::<_, _, Empty>(&router, escrow_addr.clone()) 82 | .unwrap(); 83 | assert_eq!(escrow_balance, Uint128::zero()); 84 | 85 | // send some tokens to create an escrow 86 | let arb = Addr::unchecked("arbiter"); 87 | let ben = String::from("beneficiary"); 88 | let id = "demo".to_string(); 89 | let create_msg = ReceiveMsg::Create(CreateMsg { 90 | id: id.clone(), 91 | arbiter: arb.to_string(), 92 | recipient: Some(ben.clone()), 93 | title: "some_title".to_string(), 94 | description: "some_description".to_string(), 95 | end_height: None, 96 | end_time: None, 97 | cw20_whitelist: None, 98 | }); 99 | let send_msg = Cw20ExecuteMsg::Send { 100 | contract: escrow_addr.to_string(), 101 | amount: Uint128::new(1200), 102 | msg: to_binary(&create_msg).unwrap(), 103 | }; 104 | let res = router 105 | .execute_contract(owner.clone(), cash_addr.clone(), &send_msg, &[]) 106 | .unwrap(); 107 | assert_eq!(4, res.events.len()); 108 | println!("{:?}", res.events); 109 | 110 | assert_eq!(res.events[0].ty.as_str(), "execute"); 111 | let cw20_attr = res.custom_attrs(1); 112 | println!("{:?}", cw20_attr); 113 | assert_eq!(4, cw20_attr.len()); 114 | 115 | assert_eq!(res.events[2].ty.as_str(), "execute"); 116 | let escrow_attr = res.custom_attrs(3); 117 | println!("{:?}", escrow_attr); 118 | assert_eq!(2, escrow_attr.len()); 119 | 120 | // ensure balances updated 121 | let owner_balance = cash.balance::<_, _, Empty>(&router, owner.clone()).unwrap(); 122 | assert_eq!(owner_balance, Uint128::new(3800)); 123 | let escrow_balance = cash 124 | .balance::<_, _, Empty>(&router, escrow_addr.clone()) 125 | .unwrap(); 126 | assert_eq!(escrow_balance, Uint128::new(1200)); 127 | 128 | // ensure escrow properly created 129 | let details: DetailsResponse = router 130 | .wrap() 131 | .query_wasm_smart(&escrow_addr, &QueryMsg::Details { id: id.clone() }) 132 | .unwrap(); 133 | assert_eq!(id, details.id); 134 | assert_eq!(arb, details.arbiter); 135 | assert_eq!(Some(ben.clone()), details.recipient); 136 | assert_eq!( 137 | vec![Cw20Coin { 138 | address: cash_addr.to_string(), 139 | amount: Uint128::new(1200) 140 | }], 141 | details.cw20_balance 142 | ); 143 | 144 | // release escrow 145 | let approve_msg = ExecuteMsg::Approve { id }; 146 | let _ = router 147 | .execute_contract(arb, escrow_addr.clone(), &approve_msg, &[]) 148 | .unwrap(); 149 | 150 | // ensure balances updated - release to ben 151 | let owner_balance = cash.balance::<_, _, Empty>(&router, owner).unwrap(); 152 | assert_eq!(owner_balance, Uint128::new(3800)); 153 | let escrow_balance = cash.balance::<_, _, Empty>(&router, escrow_addr).unwrap(); 154 | assert_eq!(escrow_balance, Uint128::zero()); 155 | let ben_balance = cash.balance::<_, _, Empty>(&router, ben).unwrap(); 156 | assert_eq!(ben_balance, Uint128::new(1200)); 157 | } 158 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | mod error; 3 | mod integration_test; 4 | pub mod msg; 5 | pub mod state; 6 | 7 | pub use crate::error::ContractError; 8 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | 3 | use cosmwasm_std::{Addr, Api, Coin, StdResult}; 4 | 5 | use cw20::{Cw20Coin, Cw20ReceiveMsg}; 6 | 7 | #[cw_serde] 8 | pub struct InstantiateMsg {} 9 | 10 | #[cw_serde] 11 | pub enum ExecuteMsg { 12 | Create(CreateMsg), 13 | /// Adds all sent native tokens to the contract 14 | TopUp { 15 | id: String, 16 | }, 17 | /// Set the recipient of the given escrow 18 | SetRecipient { 19 | id: String, 20 | recipient: String, 21 | }, 22 | /// Approve sends all tokens to the recipient. 23 | /// Only the arbiter can do this 24 | Approve { 25 | /// id is a human-readable name for the escrow from create 26 | id: String, 27 | }, 28 | /// Refund returns all remaining tokens to the original sender, 29 | /// The arbiter can do this any time, or anyone can do this after a timeout 30 | Refund { 31 | /// id is a human-readable name for the escrow from create 32 | id: String, 33 | }, 34 | /// This accepts a properly-encoded ReceiveMsg from a cw20 contract 35 | Receive(Cw20ReceiveMsg), 36 | } 37 | 38 | #[cw_serde] 39 | pub enum ReceiveMsg { 40 | Create(CreateMsg), 41 | /// Adds all sent native tokens to the contract 42 | TopUp { 43 | id: String, 44 | }, 45 | } 46 | 47 | #[cw_serde] 48 | pub struct CreateMsg { 49 | /// id is a human-readable name for the escrow to use later 50 | /// 3-20 bytes of utf-8 text 51 | pub id: String, 52 | /// arbiter can decide to approve or refund the escrow 53 | pub arbiter: String, 54 | /// if approved, funds go to the recipient 55 | pub recipient: Option, 56 | /// Title of the escrow 57 | pub title: String, 58 | /// Longer description of the escrow, e.g. what conditions should be met 59 | pub description: String, 60 | /// When end height set and block height exceeds this value, the escrow is expired. 61 | /// Once an escrow is expired, it can be returned to the original funder (via "refund"). 62 | pub end_height: Option, 63 | /// When end time (in seconds since epoch 00:00:00 UTC on 1 January 1970) is set and 64 | /// block time exceeds this value, the escrow is expired. 65 | /// Once an escrow is expired, it can be returned to the original funder (via "refund"). 66 | pub end_time: Option, 67 | /// Besides any possible tokens sent with the CreateMsg, this is a list of all cw20 token addresses 68 | /// that are accepted by the escrow during a top-up. This is required to avoid a DoS attack by topping-up 69 | /// with an invalid cw20 contract. See https://github.com/CosmWasm/cosmwasm-plus/issues/19 70 | pub cw20_whitelist: Option>, 71 | } 72 | 73 | impl CreateMsg { 74 | pub fn addr_whitelist(&self, api: &dyn Api) -> StdResult> { 75 | match self.cw20_whitelist.as_ref() { 76 | Some(v) => v.iter().map(|h| api.addr_validate(h)).collect(), 77 | None => Ok(vec![]), 78 | } 79 | } 80 | } 81 | 82 | pub fn is_valid_name(name: &str) -> bool { 83 | let bytes = name.as_bytes(); 84 | if bytes.len() < 3 || bytes.len() > 20 { 85 | return false; 86 | } 87 | true 88 | } 89 | 90 | #[cw_serde] 91 | #[derive(QueryResponses)] 92 | pub enum QueryMsg { 93 | /// Show all open escrows. Return type is ListResponse. 94 | #[returns(ListResponse)] 95 | List {}, 96 | /// Returns the details of the named escrow, error if not created 97 | /// Return type: DetailsResponse. 98 | #[returns(DetailsResponse)] 99 | Details { id: String }, 100 | } 101 | 102 | #[cw_serde] 103 | pub struct ListResponse { 104 | /// list all registered ids 105 | pub escrows: Vec, 106 | } 107 | 108 | #[cw_serde] 109 | pub struct DetailsResponse { 110 | /// id of this escrow 111 | pub id: String, 112 | /// arbiter can decide to approve or refund the escrow 113 | pub arbiter: String, 114 | /// if approved, funds go to the recipient 115 | pub recipient: Option, 116 | /// if refunded, funds go to the source 117 | pub source: String, 118 | /// Title of the escrow 119 | pub title: String, 120 | /// Longer description of the escrow, e.g. what conditions should be met 121 | pub description: String, 122 | /// When end height set and block height exceeds this value, the escrow is expired. 123 | /// Once an escrow is expired, it can be returned to the original funder (via "refund"). 124 | pub end_height: Option, 125 | /// When end time (in seconds since epoch 00:00:00 UTC on 1 January 1970) is set and 126 | /// block time exceeds this value, the escrow is expired. 127 | /// Once an escrow is expired, it can be returned to the original funder (via "refund"). 128 | pub end_time: Option, 129 | /// Balance in native tokens 130 | pub native_balance: Vec, 131 | /// Balance in cw20 tokens 132 | pub cw20_balance: Vec, 133 | /// Whitelisted cw20 tokens 134 | pub cw20_whitelist: Vec, 135 | } 136 | -------------------------------------------------------------------------------- /contracts/cw20-escrow/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | 3 | use cosmwasm_std::{Addr, Coin, Env, Order, StdResult, Storage, Timestamp}; 4 | use cw_storage_plus::Map; 5 | 6 | use cw20::{Balance, Cw20CoinVerified}; 7 | 8 | #[cw_serde] 9 | #[derive(Default)] 10 | pub struct GenericBalance { 11 | pub native: Vec, 12 | pub cw20: Vec, 13 | } 14 | 15 | impl GenericBalance { 16 | pub fn add_tokens(&mut self, add: Balance) { 17 | match add { 18 | Balance::Native(balance) => { 19 | for token in balance.0 { 20 | let index = self.native.iter().enumerate().find_map(|(i, exist)| { 21 | if exist.denom == token.denom { 22 | Some(i) 23 | } else { 24 | None 25 | } 26 | }); 27 | match index { 28 | Some(idx) => self.native[idx].amount += token.amount, 29 | None => self.native.push(token), 30 | } 31 | } 32 | } 33 | Balance::Cw20(token) => { 34 | let index = self.cw20.iter().enumerate().find_map(|(i, exist)| { 35 | if exist.address == token.address { 36 | Some(i) 37 | } else { 38 | None 39 | } 40 | }); 41 | match index { 42 | Some(idx) => self.cw20[idx].amount += token.amount, 43 | None => self.cw20.push(token), 44 | } 45 | } 46 | }; 47 | } 48 | } 49 | 50 | #[cw_serde] 51 | pub struct Escrow { 52 | /// arbiter can decide to approve or refund the escrow 53 | pub arbiter: Addr, 54 | /// if approved, funds go to the recipient, cannot approve if recipient is none 55 | pub recipient: Option, 56 | /// if refunded, funds go to the source 57 | pub source: Addr, 58 | /// Title of the escrow, for example for a bug bounty "Fix issue in contract.rs" 59 | pub title: String, 60 | /// Description of the escrow, a more in depth description of how to meet the escrow condition 61 | pub description: String, 62 | /// When end height set and block height exceeds this value, the escrow is expired. 63 | /// Once an escrow is expired, it can be returned to the original funder (via "refund"). 64 | pub end_height: Option, 65 | /// When end time (in seconds since epoch 00:00:00 UTC on 1 January 1970) is set and 66 | /// block time exceeds this value, the escrow is expired. 67 | /// Once an escrow is expired, it can be returned to the original funder (via "refund"). 68 | pub end_time: Option, 69 | /// Balance in Native and Cw20 tokens 70 | pub balance: GenericBalance, 71 | /// All possible contracts that we accept tokens from 72 | pub cw20_whitelist: Vec, 73 | } 74 | 75 | impl Escrow { 76 | pub fn is_expired(&self, env: &Env) -> bool { 77 | if let Some(end_height) = self.end_height { 78 | if env.block.height > end_height { 79 | return true; 80 | } 81 | } 82 | 83 | if let Some(end_time) = self.end_time { 84 | if env.block.time > Timestamp::from_seconds(end_time) { 85 | return true; 86 | } 87 | } 88 | 89 | false 90 | } 91 | 92 | pub fn human_whitelist(&self) -> Vec { 93 | self.cw20_whitelist.iter().map(|a| a.to_string()).collect() 94 | } 95 | } 96 | 97 | pub const ESCROWS: Map<&str, Escrow> = Map::new("escrow"); 98 | 99 | /// This returns the list of ids for all registered escrows 100 | pub fn all_escrow_ids(storage: &dyn Storage) -> StdResult> { 101 | ESCROWS 102 | .keys(storage, None, None, Order::Ascending) 103 | .collect() 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | 110 | use cosmwasm_std::testing::MockStorage; 111 | 112 | #[test] 113 | fn no_escrow_ids() { 114 | let storage = MockStorage::new(); 115 | let ids = all_escrow_ids(&storage).unwrap(); 116 | assert_eq!(0, ids.len()); 117 | } 118 | 119 | fn dummy_escrow() -> Escrow { 120 | Escrow { 121 | arbiter: Addr::unchecked("arb"), 122 | recipient: Some(Addr::unchecked("recip")), 123 | source: Addr::unchecked("source"), 124 | title: "some_escrow".to_string(), 125 | description: "some escrow desc".to_string(), 126 | end_height: None, 127 | end_time: None, 128 | balance: Default::default(), 129 | cw20_whitelist: vec![], 130 | } 131 | } 132 | 133 | #[test] 134 | fn all_escrow_ids_in_order() { 135 | let mut storage = MockStorage::new(); 136 | ESCROWS.save(&mut storage, "lazy", &dummy_escrow()).unwrap(); 137 | ESCROWS 138 | .save(&mut storage, "assign", &dummy_escrow()) 139 | .unwrap(); 140 | ESCROWS.save(&mut storage, "zen", &dummy_escrow()).unwrap(); 141 | 142 | let ids = all_escrow_ids(&storage).unwrap(); 143 | assert_eq!(3, ids.len()); 144 | assert_eq!( 145 | vec!["assign".to_string(), "lazy".to_string(), "zen".to_string()], 146 | ids 147 | ) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --example schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-merkle-airdrop" 3 | version = "0.14.2" 4 | authors = ["Orkun Kulce ", "Terraform Labs, PTE."] 5 | edition = "2018" 6 | description = "An Airdrop contract for allowing users to claim rewards with Merkle Tree based proof" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-tokens" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | exclude = [ 13 | "contract.wasm", 14 | "hash.txt", 15 | ] 16 | 17 | [lib] 18 | crate-type = ["cdylib", "rlib"] 19 | 20 | [features] 21 | backtraces = ["cosmwasm-std/backtraces"] 22 | library = [] 23 | 24 | [dependencies] 25 | cw-utils = "0.16.0" 26 | cw2 = "0.16.0" 27 | cw20 = "0.16.0" 28 | cosmwasm-std = "1.1.5" 29 | cw-storage-plus = "0.16.0" 30 | serde = { version = "1.0.137", default-features = false, features = ["derive"] } 31 | thiserror = "1.0.31" 32 | hex = "0.4" 33 | sha2 = { version = "0.9.9", default-features = false } 34 | ripemd = "0.1.1" 35 | bech32 = "0.9.0" 36 | cosmwasm-schema = "1.1.5" 37 | semver = "1.0.14" 38 | [dev-dependencies] 39 | cw20-base = { version = "0.16.0", features = ["library"] } 40 | cw-multi-test = "0.16.0" 41 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/NOTICE: -------------------------------------------------------------------------------- 1 | CW20-Merkle-Airdrop: A reference implementation for merkle airdrop on CosmWasm 2 | 3 | Copyright (C) 2021 Terraform Labs, PTE. 4 | Copyright (C) 2021-22 Confio GmbH 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/README.md: -------------------------------------------------------------------------------- 1 | # CW20 Merkle Airdrop 2 | 3 | This is a merkle airdrop smart contract that works with cw20 token specification Mass airdrop distributions made cheap 4 | and efficient. 5 | 6 | Explanation of merkle 7 | airdrop: [Medium Merkle Airdrop: the Basics](https://medium.com/smartz-blog/merkle-airdrop-the-basics-9a0857fcc930) 8 | 9 | Traditional and non-efficient airdrops: 10 | 11 | - Distributor creates a list of airdrop 12 | - Sends bank send messages to send tokens to recipients 13 | 14 | **Or** 15 | 16 | - Stores list of recipients on smart contract data 17 | - Recipient claims the airdrop 18 | 19 | These two solutions are very ineffective when recipient list is big. First, costly because bank send cost for the 20 | distributor will be costly. Second, whole airdrop list stored in the state, again costly. 21 | 22 | Merkle Airdrop is very efficient even when recipient number is massive. 23 | 24 | This contract works with multiple airdrop rounds, meaning you can execute several airdrops using same instance. 25 | 26 | Uses **SHA256** for merkle root tree construction. 27 | 28 | ## Procedure 29 | 30 | - Distributor of contract prepares a list of addresses with many entries and publishes this list in public static .js 31 | file in JSON format 32 | - Distributor reads this list, builds the merkle tree structure and writes down the Merkle root of it. 33 | - Distributor creates contract and places calculated Merkle root into it. 34 | - Distributor says to users, that they can claim their tokens, if they owe any of addresses, presented in list, 35 | published on distributor's site. 36 | - User wants to claim his N tokens, he also builds Merkle tree from public list and prepares Merkle proof, consisting 37 | from log2N hashes, describing the way to reach Merkle root 38 | - User sends transaction with Merkle proof to contract 39 | - Contract checks Merkle proof, and, if proof is correct, then sender's address is in list of allowed addresses, and 40 | contract does some action for this use. 41 | - Distributor sends token to the contract, and registers new merkle root for the next distribution round. 42 | 43 | ## Spec 44 | 45 | ### Messages 46 | 47 | #### InstantiateMsg 48 | 49 | `InstantiateMsg` instantiates contract with owner and cw20 token address. Airdrop `stage` is set to 0. 50 | 51 | ```rust 52 | pub struct InstantiateMsg { 53 | pub owner: String, 54 | pub cw20_token_address: String, 55 | } 56 | ``` 57 | 58 | #### ExecuteMsg 59 | 60 | ```rust 61 | pub enum ExecuteMsg { 62 | UpdateConfig { 63 | owner: Option, 64 | }, 65 | RegisterMerkleRoot { 66 | merkle_root: String, 67 | }, 68 | Claim { 69 | stage: u8, 70 | amount: Uint128, 71 | proof: Vec, 72 | }, 73 | } 74 | ``` 75 | 76 | - `UpdateConfig{owner}` updates configuration. 77 | - `RegisterMerkleRoot {merkle_root}` registers merkle tree root for further claim verification. Airdrop `Stage` 78 | increased by 1. 79 | - `Claim{stage, amount, proof}` recipient executes for claiming airdrop with `stage`, `amount` and `proof` data built 80 | using full list. 81 | 82 | #### QueryMsg 83 | 84 | ``` rust 85 | pub enum QueryMsg { 86 | Config {}, 87 | MerkleRoot { stage: u8 }, 88 | LatestStage {}, 89 | IsClaimed { stage: u8, address: String }, 90 | } 91 | ``` 92 | 93 | - `{ config: {} }` returns configuration, `{"cw20_token_address": ..., "owner": ...}`. 94 | - `{ merkle_root: { stage: "1" }` returns merkle root of given stage, `{"merkle_root": ... , "stage": ...}` 95 | - `{ latest_stage: {}}` returns current airdrop stage, `{"latest_stage": ...}` 96 | - `{ is_claimed: {stage: "stage", address: "wasm1..."}` returns if address claimed airdrop, `{"is_claimed": "true"}` 97 | 98 | ## Merkle Airdrop CLI 99 | 100 | [Merkle Airdrop CLI](helpers) contains js helpers for generating root, generating and verifying proofs for given airdrop 101 | file. 102 | 103 | ## Test Vector Generation 104 | 105 | Test vector can be generated using commands at [Merkle Airdrop CLI README](helpers/README.md) 106 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw20_merkle_airdrop::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | query: QueryMsg, 9 | execute: ExecuteMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | node_modules 9 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/README.md: -------------------------------------------------------------------------------- 1 | merkle-airdrop-cli 2 | ================== 3 | 4 | This is a helper client shipped along contract. 5 | Use this to generate root, generate proofs and verify proofs 6 | 7 | ## Installation 8 | 9 | ```shell 10 | yarn install 11 | yarn link 12 | ``` 13 | 14 | Binary will be placed to path. 15 | 16 | ## Airdrop file format 17 | 18 | ```json 19 | [ 20 | { "address": "wasm1k9hwzxs889jpvd7env8z49gad3a3633vg350tq", "amount": "100"}, 21 | { "address": "wasm1uy9ucvgerneekxpnfwyfnpxvlsx5dzdpf0mzjd", "amount": "1010"} 22 | ] 23 | ``` 24 | 25 | ## Commands 26 | 27 | **Generate Root:** 28 | ```shell 29 | merkle-airdrop-cli generateRoot --file ../testdata/airdrop_stage_2_list.json 30 | ``` 31 | 32 | **Generate proof:** 33 | ```shell 34 | merkle-airdrop-cli generateProofs --file ../testdata/airdrop_stage_2_list.json \ 35 | --address wasm1ylna88nach9sn5n7qe7u5l6lh7dmt6lp2y63xx \ 36 | --amount 1000000000 37 | ``` 38 | 39 | **Verify proof:** 40 | ```shell 41 | PROOFS='[ "27e9b1ec8cb64709d0a8d3702344561674199fe81b885f1f9c9b2fb268795962","280777995d054081cbf208bccb70f8d736c1766b81d90a1fd21cd97d2d83a5cc","3946ea1758a5a2bf55bae1186168ad35aa0329805bc8bff1ca3d51345faec04a" 42 | ]' 43 | merkle-airdrop-cli verifyProofs --file ../testdata/airdrop.json \ 44 | --address wasm1k9hwzxs889jpvd7env8z49gad3a3633vg350tq \ 45 | --amount 100 \ 46 | --proofs $PROOFS 47 | ``` 48 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .then(require('@oclif/command/flush')) 5 | .catch(require('@oclif/errors/handle')) 6 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merkle-airdrop-cli", 3 | "version": "0.1.0", 4 | "author": "Orkun Külçe @orkunkl", 5 | "bin": { 6 | "merkle-airdrop-cli": "./bin/run" 7 | }, 8 | "dependencies": { 9 | "@cosmjs/crypto": "^0.25.5", 10 | "@cosmjs/encoding": "^0.25.5", 11 | "@oclif/command": "^1", 12 | "@oclif/config": "^1", 13 | "@oclif/plugin-help": "^3", 14 | "@types/crypto-js": "^4.0.2", 15 | "ethereumjs-util": "^7.1.0", 16 | "merkletreejs": "^0.2.23", 17 | "tslib": "^1" 18 | }, 19 | "devDependencies": { 20 | "@oclif/dev-cli": "^1", 21 | "@types/node": "^10", 22 | "eslint": "^5.13", 23 | "eslint-config-oclif": "^3.1", 24 | "eslint-config-oclif-typescript": "^0.1", 25 | "globby": "^10", 26 | "ts-node": "^8", 27 | "typescript": "^3.3" 28 | }, 29 | "engines": { 30 | "node": ">=8.0.0" 31 | }, 32 | "files": [ 33 | "/bin", 34 | "/lib", 35 | "/npm-shrinkwrap.json", 36 | "/oclif.manifest.json" 37 | ], 38 | "keywords": [ 39 | "cosmwasm", 40 | "cw20" 41 | ], 42 | "license": "Apache-2.0", 43 | "main": "lib/index.js", 44 | "oclif": { 45 | "commands": "./lib/commands", 46 | "bin": "merkle-airdrop-cli", 47 | "plugins": [ 48 | "@oclif/plugin-help" 49 | ] 50 | }, 51 | "repository": "CosmWasm/cosmwasm-plus/cw20-merkle-airdrop/merkle-airdrop-cli", 52 | "scripts": { 53 | "postpack": "rm -f oclif.manifest.json", 54 | "posttest": "eslint . --ext .ts --config .eslintrc", 55 | "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme", 56 | "test": "echo NO TESTS", 57 | "version": "oclif-dev readme && git add README.md" 58 | }, 59 | "types": "lib/index.d.ts" 60 | } 61 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/src/airdrop.ts: -------------------------------------------------------------------------------- 1 | import sha256 from 'crypto-js/sha256' 2 | import { MerkleTree } from 'merkletreejs'; 3 | 4 | class Airdrop { 5 | private tree: MerkleTree; 6 | 7 | constructor(accounts: Array<{ address: string; amount: string }>) { 8 | const leaves = accounts.map((a) => sha256(a.address + a.amount)); 9 | this.tree = new MerkleTree(leaves, sha256, { sort: true }); 10 | } 11 | 12 | public getMerkleRoot(): string { 13 | return this.tree.getHexRoot().replace('0x', ''); 14 | } 15 | 16 | public getMerkleProof(account: { 17 | address: string; 18 | amount: string; 19 | }): string[] { 20 | return this.tree 21 | .getHexProof(sha256(account.address + account.amount).toString()) 22 | .map((v) => v.replace('0x', '')); 23 | } 24 | 25 | public verify( 26 | proof: string[], 27 | account: { address: string; amount: string } 28 | ): boolean { 29 | let hashBuf = Buffer.from(sha256(account.address + account.amount).toString()) 30 | 31 | proof.forEach((proofElem) => { 32 | const proofBuf = Buffer.from(proofElem, 'hex'); 33 | if (hashBuf < proofBuf) { 34 | hashBuf = Buffer.from(sha256(Buffer.concat([hashBuf, proofBuf]).toString())); 35 | } else { 36 | hashBuf = Buffer.from(sha256(Buffer.concat([proofBuf, hashBuf]).toString())); 37 | } 38 | }); 39 | 40 | return this.getMerkleRoot() === hashBuf.toString('hex'); 41 | } 42 | } 43 | 44 | export {Airdrop} 45 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/src/commands/generateProofs.ts: -------------------------------------------------------------------------------- 1 | import {Command, flags} from '@oclif/command' 2 | import { readFileSync } from 'fs'; 3 | import {Airdrop} from '../airdrop'; 4 | 5 | export default class GenerateProof extends Command { 6 | static description = 'Generates merkle proofs for given address' 7 | 8 | static examples = [ 9 | `$ merkle-airdrop-cli generateProofs --file ../testdata/airdrop_stage_2.json \ 10 | --address wasm1ylna88nach9sn5n7qe7u5l6lh7dmt6lp2y63xx \ 11 | --amount 1000000000 12 | `, 13 | ] 14 | 15 | static flags = { 16 | help: flags.help({char: 'h'}), 17 | file: flags.string({char: 'f', description: 'airdrop file location'}), 18 | address: flags.string({char: 'a', description: 'address'}), 19 | amount: flags.string({char: 'b', description: 'amount'}), 20 | } 21 | 22 | async run() { 23 | const {flags} = this.parse(GenerateProof) 24 | 25 | if (!flags.file) { 26 | this.error(new Error('Airdrop file location not defined')) 27 | } 28 | if (!flags.address) { 29 | this.error(new Error('Address not defined')) 30 | } 31 | if (!flags.amount) { 32 | this.error(new Error('Amount not defined')) 33 | } 34 | 35 | let file; 36 | try { 37 | file = readFileSync(flags.file, 'utf-8'); 38 | } catch (e) { 39 | this.error(e) 40 | } 41 | 42 | let receivers: Array<{ address: string; amount: string }> = JSON.parse(file); 43 | 44 | let airdrop = new Airdrop(receivers) 45 | let proof = airdrop.getMerkleProof({address: flags.address, amount: flags.amount}) 46 | console.log(proof) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/src/commands/generateRoot.ts: -------------------------------------------------------------------------------- 1 | import {Command, flags} from '@oclif/command' 2 | import { readFileSync } from 'fs'; 3 | import {Airdrop} from '../airdrop'; 4 | 5 | export default class GenerateRoot extends Command { 6 | static description = 'Generates merkle root' 7 | 8 | static examples = [ 9 | `$ merkle-airdrop-cli generateRoot --file ../testdata/airdrop_stage_2.json 10 | `, 11 | ] 12 | 13 | static flags = { 14 | help: flags.help({char: 'h'}), 15 | file: flags.string({char: 'f', description: 'Airdrop file location'}), 16 | } 17 | 18 | async run() { 19 | const {flags} = this.parse(GenerateRoot) 20 | 21 | if (!flags.file) { 22 | this.error(new Error('Airdrop file location not defined')) 23 | } 24 | 25 | let file; 26 | try { 27 | file = readFileSync(flags.file, 'utf-8'); 28 | } catch (e) { 29 | this.error(e) 30 | } 31 | 32 | let receivers: Array<{ address: string; amount: string }> = JSON.parse(file); 33 | 34 | let airdrop = new Airdrop(receivers) 35 | console.log(airdrop.getMerkleRoot()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/src/commands/verifyProofs.ts: -------------------------------------------------------------------------------- 1 | import {Command, flags} from '@oclif/command' 2 | import { readFileSync } from 'fs'; 3 | import {Airdrop} from '../airdrop'; 4 | 5 | export default class VerifyProof extends Command { 6 | static description = 'Verifies merkle proofs for given address' 7 | 8 | static examples = [ 9 | `$ PROOFS='[ "27e9b1ec8cb64709d0a8d3702344561674199fe81b885f1f9c9b2fb268795962","280777995d054081cbf208bccb70f8d736c1766b81d90a1fd21cd97d2d83a5cc","3946ea1758a5a2bf55bae1186168ad35aa0329805bc8bff1ca3d51345faec04a"]' 10 | $ merkle-airdrop-cli verifyProofs --file ../testdata/airdrop.json \ 11 | --address wasm1k9hwzxs889jpvd7env8z49gad3a3633vg350tq \ 12 | --amount 100 13 | --proofs $PROOFS 14 | `, 15 | ] 16 | 17 | static flags = { 18 | help: flags.help({char: 'h'}), 19 | file: flags.string({char: 'f', description: 'airdrop file location'}), 20 | proofs: flags.string({char: 'p', description: 'proofs in json format'}), 21 | address: flags.string({char: 'a', description: 'address'}), 22 | amount: flags.string({char: 'b', description: 'amount'}), 23 | } 24 | 25 | async run() { 26 | const {flags} = this.parse(VerifyProof) 27 | 28 | if (!flags.file) { 29 | this.error(new Error('Airdrop file location not defined')) 30 | } 31 | if (!flags.proofs) { 32 | this.error(new Error('Proofs not defined')) 33 | } 34 | if (!flags.address) { 35 | this.error(new Error('Address not defined')) 36 | } 37 | if (!flags.amount) { 38 | this.error(new Error('Amount not defined')) 39 | } 40 | 41 | let file; 42 | try { 43 | file = readFileSync(flags.file, 'utf-8'); 44 | } catch (e) { 45 | this.error(e) 46 | } 47 | 48 | let receivers: Array<{ address: string; amount: string }> = JSON.parse(file); 49 | 50 | let airdrop = new Airdrop(receivers) 51 | let proofs: string[] = JSON.parse(flags.proofs) 52 | 53 | console.log(airdrop.verify(proofs, {address: flags.address, amount: flags.amount})) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/command' 2 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "importHelpers": true, 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "strict": true, 7 | "target": "es2017", 8 | "allowSyntheticDefaultImports": true, 9 | "alwaysStrict": true, 10 | "baseUrl": "./", 11 | "declaration": true, 12 | "esModuleInterop": true, 13 | "lib": ["es2015", "es2016", "es2017", "dom"], 14 | "module": "commonjs", 15 | "moduleResolution": "node", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": true, 22 | "sourceMap": true, 23 | "strictFunctionTypes": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true, 26 | "paths": { 27 | "*": ["src/*"] 28 | } 29 | }, 30 | "include": [ 31 | "src/**/*" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/src/enumerable.rs: -------------------------------------------------------------------------------- 1 | use crate::msg::{AccountMapResponse, AllAccountMapResponse}; 2 | use crate::state::STAGE_ACCOUNT_MAP; 3 | use cosmwasm_std::{Deps, Order, StdResult}; 4 | use cw_storage_plus::Bound; 5 | 6 | // settings for pagination 7 | const MAX_LIMIT: u32 = 1000; 8 | const DEFAULT_LIMIT: u32 = 10; 9 | 10 | pub fn query_all_address_map( 11 | deps: Deps, 12 | stage: u8, 13 | start_after: Option, 14 | limit: Option, 15 | ) -> StdResult { 16 | let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; 17 | let start = start_after.map(Bound::exclusive); 18 | 19 | let address_maps = STAGE_ACCOUNT_MAP 20 | .prefix(stage) 21 | .range(deps.storage, start, None, Order::Ascending) 22 | .take(limit) 23 | .map(|p| { 24 | p.map(|(external_address, host_address)| AccountMapResponse { 25 | host_address, 26 | external_address, 27 | }) 28 | }) 29 | .collect::>()?; 30 | 31 | let resp = AllAccountMapResponse { address_maps }; 32 | Ok(resp) 33 | } 34 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{StdError, Uint128}; 2 | use cw_utils::{Expiration, Scheduled}; 3 | use hex::FromHexError; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug, PartialEq)] 7 | pub enum ContractError { 8 | #[error("{0}")] 9 | Std(#[from] StdError), 10 | 11 | #[error("{0}")] 12 | Hex(#[from] FromHexError), 13 | 14 | #[error("Unauthorized")] 15 | Unauthorized {}, 16 | 17 | #[error("Invalid input")] 18 | InvalidInput {}, 19 | 20 | #[error("Already claimed")] 21 | Claimed {}, 22 | 23 | #[error("Wrong length")] 24 | WrongLength {}, 25 | 26 | #[error("Verification failed")] 27 | VerificationFailed {}, 28 | 29 | #[error("Invalid token type")] 30 | InvalidTokenType {}, 31 | 32 | #[error("Insufficient Funds: Contract balance: {balance} does not cover the required amount: {amount}")] 33 | InsufficientFunds { balance: Uint128, amount: Uint128 }, 34 | 35 | #[error("Cannot migrate from different contract type: {previous_contract}")] 36 | CannotMigrate { previous_contract: String }, 37 | 38 | #[error("Airdrop stage {stage} expired at {expiration}")] 39 | StageExpired { stage: u8, expiration: Expiration }, 40 | 41 | #[error("Airdrop stage {stage} not expired yet")] 42 | StageNotExpired { stage: u8, expiration: Expiration }, 43 | 44 | #[error("Airdrop stage {stage} begins at {start}")] 45 | StageNotBegun { stage: u8, start: Scheduled }, 46 | 47 | #[error("Airdrop stage {stage} is paused")] 48 | StagePaused { stage: u8 }, 49 | 50 | #[error("Airdrop stage {stage} is not paused")] 51 | StageNotPaused { stage: u8 }, 52 | 53 | #[error("Semver parsing error: {0}")] 54 | SemVer(String), 55 | } 56 | 57 | impl From for ContractError { 58 | fn from(err: semver::Error) -> Self { 59 | Self::SemVer(err.to_string()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::ContractError; 2 | use bech32::ToBase32; 3 | use cosmwasm_schema::cw_serde; 4 | use cosmwasm_std::{Binary, Deps}; 5 | use ripemd::{Digest as RipDigest, Ripemd160}; 6 | use sha2::{Digest as ShaDigest, Sha256}; 7 | use std::convert::TryInto; 8 | 9 | #[cw_serde] 10 | pub struct CosmosSignature { 11 | pub pub_key: Binary, 12 | pub signature: Binary, 13 | } 14 | impl CosmosSignature { 15 | pub fn verify(&self, deps: Deps, claim_msg: &Binary) -> Result { 16 | let hash = Sha256::digest(claim_msg); 17 | 18 | deps.api 19 | .secp256k1_verify( 20 | hash.as_ref(), 21 | self.signature.as_slice(), 22 | self.pub_key.as_slice(), 23 | ) 24 | .map_err(|_| ContractError::VerificationFailed {}) 25 | } 26 | 27 | pub fn derive_addr_from_pubkey(&self, hrp: &str) -> Result { 28 | // derive external address for merkle proof check 29 | let sha_hash: [u8; 32] = Sha256::digest(self.pub_key.as_slice()) 30 | .as_slice() 31 | .try_into() 32 | .map_err(|_| ContractError::WrongLength {})?; 33 | 34 | let rip_hash = Ripemd160::digest(sha_hash); 35 | let rip_slice: &[u8] = rip_hash.as_slice(); 36 | 37 | let addr: String = bech32::encode(hrp, rip_slice.to_base32(), bech32::Variant::Bech32) 38 | .map_err(|_| ContractError::VerificationFailed {})?; 39 | Ok(addr) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | mod enumerable; 3 | mod error; 4 | pub mod helpers; 5 | pub mod migrations; 6 | pub mod msg; 7 | pub mod state; 8 | 9 | pub use crate::error::ContractError; 10 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/src/migrations.rs: -------------------------------------------------------------------------------- 1 | // Migration logic for contracts with version: 0.12.1 2 | pub mod v0_12_1 { 3 | use crate::state::{LATEST_STAGE, STAGE_PAUSED}; 4 | use crate::ContractError; 5 | use cosmwasm_std::DepsMut; 6 | pub fn set_initial_pause_status(deps: DepsMut) -> Result<(), ContractError> { 7 | let latest_stage = LATEST_STAGE.load(deps.storage)?; 8 | for stage in 0..=latest_stage { 9 | STAGE_PAUSED.save(deps.storage, stage, &false)?; 10 | } 11 | Ok(()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/src/msg.rs: -------------------------------------------------------------------------------- 1 | use crate::ContractError; 2 | use cosmwasm_schema::{cw_serde, QueryResponses}; 3 | use cosmwasm_std::{from_slice, Binary, Uint128}; 4 | use cw_utils::{Expiration, Scheduled}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[cw_serde] 8 | pub struct InstantiateMsg { 9 | /// Owner if none set to info.sender. 10 | pub owner: Option, 11 | pub cw20_token_address: Option, 12 | pub native_token: Option, 13 | } 14 | 15 | #[cw_serde] 16 | pub enum ExecuteMsg { 17 | UpdateConfig { 18 | /// NewOwner if non sent, contract gets locked. Recipients can receive airdrops 19 | /// but owner cannot register new stages. 20 | new_owner: Option, 21 | new_cw20_address: Option, 22 | new_native_token: Option, 23 | }, 24 | RegisterMerkleRoot { 25 | /// MerkleRoot is hex-encoded merkle root. 26 | merkle_root: String, 27 | expiration: Option, 28 | start: Option, 29 | total_amount: Option, 30 | // hrp is the bech32 parameter required for building external network address 31 | // from signature message during claim action. example "cosmos", "terra", "juno" 32 | hrp: Option, 33 | }, 34 | /// Claim does not check if contract has enough funds, owner must ensure it. 35 | Claim { 36 | stage: u8, 37 | amount: Uint128, 38 | /// Proof is hex-encoded merkle proof. 39 | proof: Vec, 40 | /// Enables cross chain airdrops. 41 | /// Target wallet proves identity by sending a signed [SignedClaimMsg](SignedClaimMsg) 42 | /// containing the recipient address. 43 | sig_info: Option, 44 | }, 45 | /// Burn the remaining tokens in the stage after expiry time (only owner) 46 | Burn { 47 | stage: u8, 48 | }, 49 | /// Withdraw the remaining tokens in the stage after expiry time (only owner) 50 | Withdraw { 51 | stage: u8, 52 | address: String, 53 | }, 54 | /// Burn all of the remaining tokens that the contract owns (only owner) 55 | BurnAll {}, 56 | /// Withdraw all/some of the remaining tokens that the contract owns (only owner) 57 | WithdrawAll { 58 | address: String, 59 | amount: Option, 60 | }, 61 | Pause { 62 | stage: u8, 63 | }, 64 | Resume { 65 | stage: u8, 66 | new_expiration: Option, 67 | }, 68 | } 69 | 70 | #[cw_serde] 71 | #[derive(QueryResponses)] 72 | pub enum QueryMsg { 73 | #[returns(ConfigResponse)] 74 | Config {}, 75 | #[returns(MerkleRootResponse)] 76 | MerkleRoot { stage: u8 }, 77 | #[returns(LatestStageResponse)] 78 | LatestStage {}, 79 | #[returns(IsClaimedResponse)] 80 | IsClaimed { stage: u8, address: String }, 81 | #[returns(TotalClaimedResponse)] 82 | TotalClaimed { stage: u8 }, 83 | // for cross chain airdrops, maps target account to host account 84 | #[returns(AccountMapResponse)] 85 | AccountMap { stage: u8, external_address: String }, 86 | #[returns(AllAccountMapResponse)] 87 | AllAccountMaps { 88 | stage: u8, 89 | start_after: Option, 90 | limit: Option, 91 | }, 92 | #[returns(IsPausedResponse)] 93 | IsPaused { stage: u8 }, 94 | } 95 | 96 | #[cw_serde] 97 | pub struct ConfigResponse { 98 | pub owner: Option, 99 | pub cw20_token_address: Option, 100 | pub native_token: Option, 101 | } 102 | 103 | #[cw_serde] 104 | pub struct MerkleRootResponse { 105 | pub stage: u8, 106 | /// MerkleRoot is hex-encoded merkle root. 107 | pub merkle_root: String, 108 | pub expiration: Expiration, 109 | pub start: Option, 110 | pub total_amount: Uint128, 111 | } 112 | 113 | #[cw_serde] 114 | pub struct LatestStageResponse { 115 | pub latest_stage: u8, 116 | } 117 | 118 | #[cw_serde] 119 | pub struct IsClaimedResponse { 120 | pub is_claimed: bool, 121 | } 122 | 123 | #[cw_serde] 124 | pub struct IsPausedResponse { 125 | pub is_paused: bool, 126 | } 127 | 128 | #[cw_serde] 129 | pub struct TotalClaimedResponse { 130 | pub total_claimed: Uint128, 131 | } 132 | 133 | #[cw_serde] 134 | pub struct AccountMapResponse { 135 | pub host_address: String, 136 | pub external_address: String, 137 | } 138 | 139 | #[cw_serde] 140 | pub struct AllAccountMapResponse { 141 | pub address_maps: Vec, 142 | } 143 | 144 | #[cw_serde] 145 | pub struct MigrateMsg {} 146 | 147 | // Signature verification is done on external airdrop claims. 148 | #[cw_serde] 149 | pub struct SignatureInfo { 150 | pub claim_msg: Binary, 151 | pub signature: Binary, 152 | } 153 | impl SignatureInfo { 154 | pub fn extract_addr(&self) -> Result { 155 | let claim_msg = from_slice::(&self.claim_msg)?; 156 | Ok(claim_msg.address) 157 | } 158 | } 159 | 160 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 161 | pub struct ClaimMsg { 162 | // To provide claiming via ledger, the address is passed in the memo field of a cosmos msg. 163 | #[serde(rename = "memo")] 164 | address: String, 165 | } 166 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | 3 | use cosmwasm_std::{Addr, Uint128}; 4 | use cw_storage_plus::{Item, Map}; 5 | use cw_utils::{Expiration, Scheduled}; 6 | 7 | #[cw_serde] 8 | pub struct Config { 9 | /// Owner If None set, contract is frozen. 10 | pub owner: Option, 11 | pub cw20_token_address: Option, 12 | pub native_token: Option, 13 | } 14 | 15 | pub const CONFIG_KEY: &str = "config"; 16 | pub const CONFIG: Item = Item::new(CONFIG_KEY); 17 | 18 | pub const LATEST_STAGE_KEY: &str = "stage"; 19 | pub const LATEST_STAGE: Item = Item::new(LATEST_STAGE_KEY); 20 | 21 | pub const STAGE_EXPIRATION_KEY: &str = "stage_exp"; 22 | pub const STAGE_EXPIRATION: Map = Map::new(STAGE_EXPIRATION_KEY); 23 | 24 | pub const STAGE_START_KEY: &str = "stage_start"; 25 | pub const STAGE_START: Map = Map::new(STAGE_START_KEY); 26 | 27 | pub const STAGE_AMOUNT_KEY: &str = "stage_amount"; 28 | pub const STAGE_AMOUNT: Map = Map::new(STAGE_AMOUNT_KEY); 29 | 30 | pub const STAGE_AMOUNT_CLAIMED_KEY: &str = "stage_claimed_amount"; 31 | pub const STAGE_AMOUNT_CLAIMED: Map = Map::new(STAGE_AMOUNT_CLAIMED_KEY); 32 | 33 | // saves external network airdrop accounts 34 | pub const STAGE_ACCOUNT_MAP_KEY: &str = "stage_account_map"; 35 | // (stage, external_address) -> host_address 36 | pub const STAGE_ACCOUNT_MAP: Map<(u8, String), String> = Map::new(STAGE_ACCOUNT_MAP_KEY); 37 | 38 | pub const MERKLE_ROOT_PREFIX: &str = "merkle_root"; 39 | pub const MERKLE_ROOT: Map = Map::new(MERKLE_ROOT_PREFIX); 40 | 41 | pub const CLAIM_PREFIX: &str = "claim"; 42 | pub const CLAIM: Map<(String, u8), bool> = Map::new(CLAIM_PREFIX); 43 | 44 | pub const CLAIMED_AMOUNT_PREFIX: &str = "claimed_amount"; 45 | pub const CLAIMED_AMOUNT: Map<(&Addr, u8), bool> = Map::new(CLAIMED_AMOUNT_PREFIX); 46 | 47 | pub const HRP_PREFIX: &str = "hrp"; 48 | pub const HRP: Map = Map::new(HRP_PREFIX); 49 | 50 | pub const STAGE_PAUSED_KEY: &str = "stage_paused"; 51 | pub const STAGE_PAUSED: Map = Map::new(STAGE_PAUSED_KEY); 52 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/testdata/airdrop_external_sig_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "address": "terra1ce2uyed946x9r9dejutepqg0myqaudwlz58uw9", "amount": "100"}, 3 | { "address": "terra17pdzgde6lyxzdjzzsze64uev7gte6ug48tzqe9", "amount": "100"}, 4 | { "address": "terra1lj2dyl9dper0cnnk4hn3pydudrvj6hvpdx850v", "amount": "100"} 5 | ] 6 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/testdata/airdrop_external_sig_test_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": "terra1fez59sv8ur9734ffrpvwpcjadx7n0x6z6xwp7z", 3 | "root": "06baae7019d60dbb614ec44afb5c8e375eab0c8c6ddcd81fe2363ff17d2554a9", 4 | "hrp": "terra", 5 | "amount": "1000000", 6 | "proofs": [], 7 | "signed_msg": { 8 | "claim_msg": "eyJhY2NvdW50X251bWJlciI6IjExMjM2IiwiY2hhaW5faWQiOiJwaXNjby0xIiwiZmVlIjp7ImFtb3VudCI6W3siYW1vdW50IjoiMTU4MTIiLCJkZW5vbSI6InVsdW5hIn1dLCJnYXMiOiIxMDU0MDcifSwibWVtbyI6Imp1bm8xMHMydXU5MjY0ZWhscWw1ZnB5cmg5dW5kbmw1bmxhdzYzdGQwaGgiLCJtc2dzIjpbeyJ0eXBlIjoiY29zbW9zLXNkay9Nc2dTZW5kIiwidmFsdWUiOnsiYW1vdW50IjpbeyJhbW91bnQiOiIxIiwiZGVub20iOiJ1bHVuYSJ9XSwiZnJvbV9hZGRyZXNzIjoidGVycmExZmV6NTlzdjh1cjk3MzRmZnJwdndwY2phZHg3bjB4Nno2eHdwN3oiLCJ0b19hZGRyZXNzIjoidGVycmExZmV6NTlzdjh1cjk3MzRmZnJwdndwY2phZHg3bjB4Nno2eHdwN3oifX1dLCJzZXF1ZW5jZSI6IjAifQ==", 9 | "signature": "eyJwdWJfa2V5IjoiQWhOZ2UxV01aVXl1ODZ5VGx5ZWpEdVVxUFZTdURONUJhQzArdkw4b3RkSnYiLCJzaWduYXR1cmUiOiJQY1FPczhXSDVPMndXL3Z3ZzZBTElqaW9VNGorMUZYNTZKU1R1MzdIb2lGbThJck5aem5HaGlIRFV1R1VTUmlhVnZRZ2s4Q0tURmNyeVpuYjZLNVhyQT09In0=" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/testdata/airdrop_stage_1_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "address": "wasm1k9hwzxs889jpvd7env8z49gad3a3633vg350tq", "amount": "100"}, 3 | { "address": "wasm1uy9ucvgerneekxpnfwyfnpxvlsx5dzdpf0mzjd", "amount": "1010"}, 4 | { "address": "wasm1a4x6au55s0fusctyj2ulrxvfpmjcxa92k7ze2v", "amount": "10220"}, 5 | { "address": "wasm1ylna88nach9sn5n7qe7u5l6lh7dmt6lp2y63xx", "amount": "10333"}, 6 | { "address": "wasm1qzy8rg0f406uvvl54dlww6ptlh30303xq2u3xu", "amount": "10220"}, 7 | { "address": "wasm1xn46zz5m3fhymcrcwe82m0ac8ytt588dkpaeas", "amount": "10220"} 8 | ] 9 | 10 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/testdata/airdrop_stage_1_test_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": "wasm1k9hwzxs889jpvd7env8z49gad3a3633vg350tq", 3 | "amount": "100", 4 | "root": "b45c1ea28b26adb13e412933c9e055b01fdf7585304b00cd8f1cb220aa6c5e88", 5 | "proofs": [ 6 | "a714186eaedddde26b08b9afda38cf62fdf88d68e3aa0d5a4b55033487fe14a1", 7 | "fb57090a813128eeb953a4210dd64ee73d2632b8158231effe2f0a18b2d3b5dd", 8 | "c30992d264c74c58b636a31098c6c27a5fc08b3f61b7eafe2a33dcb445822343" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/testdata/airdrop_stage_1_test_multi_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_amount": "42103", 3 | "total_claimed_amount": "21663", 4 | "root": "b45c1ea28b26adb13e412933c9e055b01fdf7585304b00cd8f1cb220aa6c5e88", 5 | "accounts": [ 6 | { 7 | "account": "wasm1k9hwzxs889jpvd7env8z49gad3a3633vg350tq", 8 | "amount": "100", 9 | "proofs": [ 10 | "a714186eaedddde26b08b9afda38cf62fdf88d68e3aa0d5a4b55033487fe14a1", 11 | "fb57090a813128eeb953a4210dd64ee73d2632b8158231effe2f0a18b2d3b5dd", 12 | "c30992d264c74c58b636a31098c6c27a5fc08b3f61b7eafe2a33dcb445822343" 13 | ] 14 | }, 15 | { 16 | "account": "wasm1uy9ucvgerneekxpnfwyfnpxvlsx5dzdpf0mzjd", 17 | "amount": "1010", 18 | "proofs": [ 19 | "d496b14f0a6207db1c9a1be70d5f3684d3c76f27c0bc75ee979f3e2a71a97ed0", 20 | "e3746c7f0e1d1f60708f9e5facaaee77424a8c5f6527f1813f60e8c3755d3b5d", 21 | "c30992d264c74c58b636a31098c6c27a5fc08b3f61b7eafe2a33dcb445822343" 22 | ] 23 | }, 24 | { 25 | "account": "wasm1a4x6au55s0fusctyj2ulrxvfpmjcxa92k7ze2v", 26 | "amount": "10220", 27 | "proofs": [ 28 | "b69c5239d434753af2f6c3eab47f4e78c436f862f14e6989be5c9027c2b6dfe2", 29 | "e3746c7f0e1d1f60708f9e5facaaee77424a8c5f6527f1813f60e8c3755d3b5d", 30 | "c30992d264c74c58b636a31098c6c27a5fc08b3f61b7eafe2a33dcb445822343" 31 | ] 32 | }, 33 | { 34 | "account": "wasm1ylna88nach9sn5n7qe7u5l6lh7dmt6lp2y63xx", 35 | "amount": "10333", 36 | "proofs": [ 37 | "f89c4ec6a98e26fb5690e50e16e189f9942f0576a5ba711ed75fe01140ddb2af", 38 | "374f1a32b0a5d5dab16f8fbed8c248e183448732f897002375e0d4ca6e13ad73" 39 | ] 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/testdata/airdrop_stage_2_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "address": "wasm1k9hwzxs889jpvd7env8z49gad3a3633vg350tq", "amount": "666666666"}, 3 | { "address": "wasm1uy9ucvgerneekxpnfwyfnpxvlsx5dzdpf0mzjd", "amount": "1010"}, 4 | { "address": "wasm1a4x6au55s0fusctyj2ulrxvfpmjcxa92k7ze2v", "amount": "999"}, 5 | { "address": "wasm1ylna88nach9sn5n7qe7u5l6lh7dmt6lp2y63xx", "amount": "1000000000"}, 6 | { "address": "wasm1qzy8rg0f406uvvl54dlww6ptlh30303xq2u3xu", "amount": "10220"}, 7 | { "address": "wasm1c99d6aw39e027fmy5f2gj38g8p8c3cf0vn3qqn", "amount": "1322"}, 8 | { "address": "wasm1uwcjkghqlz030r989clzqs8zlaujwyphx0yumy", "amount": "14"}, 9 | { "address": "wasm1yggt0x0r3x5ujk96kfeps6v4yakgun8mdth90j", "amount": "9000000"}, 10 | { "address": "wasm1f6s77fjplerjrh4yjj08msqdq36mam4xv9tjvs", "amount": "12333"}, 11 | { "address": "wasm1xn46zz5m3fhymcrcwe82m0ac8ytt588dkpaeas", "amount": "1322"} 12 | ] 13 | 14 | -------------------------------------------------------------------------------- /contracts/cw20-merkle-airdrop/testdata/airdrop_stage_2_test_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": "wasm1uwcjkghqlz030r989clzqs8zlaujwyphx0yumy", 3 | "amount": "14", 4 | "root": "a5587bd4d158618b83badf57b1a4206f86e33407e18797ef690c931d73b36232", 5 | "proofs": [ 6 | "a714186eaedddde26b08b9afda38cf62fdf88d68e3aa0d5a4b55033487fe14a1", 7 | "1eb08e61c40d5ba334f3c32f3f136e714f0841e5d53af6b78ec94e3b29a01e74", 8 | "fe570ffb0015447c01bffdcd266fe4ee21a23eb6b499461b9ced5a03c6a9b2f0", 9 | "fa0224da936bcebd0f018a46ba15a5a9fc2d637f72f7c14b31aeffd8964983b5" 10 | ] 11 | } -------------------------------------------------------------------------------- /contracts/cw20-staking/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --example schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-staking/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-staking" 3 | version = "0.14.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Implement simple staking derivatives as a cw20 token" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-tokens" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | 17 | [features] 18 | backtraces = ["cosmwasm-std/backtraces"] 19 | # use library feature to disable all instantiate/execute/query exports 20 | library = [] 21 | 22 | [dependencies] 23 | cw-utils = "0.16.0" 24 | cw2 = "0.16.0" 25 | cw20 = "0.16.0" 26 | cw-controllers = "0.16.0" 27 | cw20-base = { version = "0.16.0", features = ["library"] } 28 | cosmwasm-std = { version = "1.1.5", features = ["staking"] } 29 | cw-storage-plus = "0.16.0" 30 | thiserror = "1.0.31" 31 | cosmwasm-schema = "1.1.5" 32 | [dev-dependencies] 33 | 34 | -------------------------------------------------------------------------------- /contracts/cw20-staking/NOTICE: -------------------------------------------------------------------------------- 1 | CW20-Staking: Staking Derivatives as a CW20 token 2 | Copyright 2020-21 Ethan Frey 3 | Copyright 2021-22 Confio GmbH 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /contracts/cw20-staking/README.md: -------------------------------------------------------------------------------- 1 | # Staking Derivatives 2 | 3 | This is a sample contract that releases a minimal form of staking derivatives. 4 | This is to be used for integration tests and as a foundation for other to build 5 | more complex logic upon. 6 | 7 | ## Functionality 8 | 9 | On one side, this acts as a CW20 token, holding a list of 10 | balances for multiple addresses, and exposing queries and transfers (no 11 | allowances and "transfer from" to focus the logic on the staking stuff). 12 | However, it has no initial balance. Instead, it mints and burns them based on 13 | delegations. 14 | 15 | For such a "bonding curve" we expose two additional message types. A "bond" 16 | message sends native staking tokens to the contract to be bonded to a validator 17 | and credits the user with the appropriate amount of derivative tokens. Likewise 18 | you can burn some of your derivative tokens, and the contract will unbond the 19 | proportional amount of stake to the user's account (after typical 21-day 20 | unbonding period). 21 | 22 | To show an example of charging for such a service, we allow the contract owner 23 | to take a small exit tax, thus maybe 98% of the tokens will be unbonded and sent 24 | to the original account, and 2% of the tokens are not unbonded, but rather 25 | transferred to the owners account. (The ownership can also be transferred). 26 | -------------------------------------------------------------------------------- /contracts/cw20-staking/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw20_staking::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | query: QueryMsg, 9 | execute: ExecuteMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw20-staking/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{StdError, Uint128}; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, PartialEq)] 5 | pub enum ContractError { 6 | #[error("{0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("Unauthorized")] 10 | Unauthorized {}, 11 | 12 | #[error("Validator '{validator}' not in current validator set")] 13 | NotInValidatorSet { validator: String }, 14 | 15 | #[error("Different denominations in bonds: '{denom1}' vs. '{denom2}'")] 16 | DifferentBondDenom { denom1: String, denom2: String }, 17 | 18 | #[error("Stored bonded {stored}, but query bonded {queried}")] 19 | BondedMismatch { stored: Uint128, queried: Uint128 }, 20 | 21 | #[error("No {denom} tokens sent")] 22 | EmptyBalance { denom: String }, 23 | 24 | #[error("Must unbond at least {min_bonded} {denom}")] 25 | UnbondTooSmall { min_bonded: Uint128, denom: String }, 26 | 27 | #[error("Insufficient balance in contract to process claim")] 28 | BalanceTooSmall {}, 29 | 30 | #[error("No claims that can be released currently")] 31 | NothingToClaim {}, 32 | 33 | #[error("Cannot set to own account")] 34 | CannotSetOwnAccount {}, 35 | 36 | #[error("Invalid expiration")] 37 | InvalidExpiration {}, 38 | 39 | #[error("Invalid zero amount")] 40 | InvalidZeroAmount {}, 41 | 42 | #[error("Allowance is expired")] 43 | Expired {}, 44 | 45 | #[error("No allowance for this account")] 46 | NoAllowance {}, 47 | 48 | #[error("Minting cannot exceed the cap")] 49 | CannotExceedCap {}, 50 | 51 | #[error("Duplicate initial balance addresses")] 52 | DuplicateInitialBalanceAddresses {}, 53 | } 54 | 55 | impl From for ContractError { 56 | fn from(err: cw20_base::ContractError) -> Self { 57 | match err { 58 | cw20_base::ContractError::Std(error) => ContractError::Std(error), 59 | cw20_base::ContractError::Unauthorized {} => ContractError::Unauthorized {}, 60 | cw20_base::ContractError::CannotSetOwnAccount {} => { 61 | ContractError::CannotSetOwnAccount {} 62 | } 63 | cw20_base::ContractError::InvalidExpiration {} => ContractError::InvalidExpiration {}, 64 | cw20_base::ContractError::InvalidZeroAmount {} => ContractError::InvalidZeroAmount {}, 65 | cw20_base::ContractError::Expired {} => ContractError::Expired {}, 66 | cw20_base::ContractError::NoAllowance {} => ContractError::NoAllowance {}, 67 | cw20_base::ContractError::CannotExceedCap {} => ContractError::CannotExceedCap {}, 68 | // This should never happen, as this contract doesn't use logo 69 | cw20_base::ContractError::LogoTooBig {} 70 | | cw20_base::ContractError::InvalidPngHeader {} 71 | | cw20_base::ContractError::InvalidXmlPreamble {} => { 72 | ContractError::Std(StdError::generic_err(err.to_string())) 73 | } 74 | cw20_base::ContractError::DuplicateInitialBalanceAddresses {} => { 75 | ContractError::DuplicateInitialBalanceAddresses {} 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /contracts/cw20-staking/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | mod error; 3 | pub mod msg; 4 | pub mod state; 5 | 6 | pub use crate::error::ContractError; 7 | -------------------------------------------------------------------------------- /contracts/cw20-staking/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | 3 | use cosmwasm_std::{Binary, Coin, Decimal, Uint128}; 4 | use cw20::Expiration; 5 | use cw20::{AllowanceResponse, BalanceResponse, TokenInfoResponse}; 6 | pub use cw_controllers::ClaimsResponse; 7 | use cw_utils::Duration; 8 | 9 | #[cw_serde] 10 | pub struct InstantiateMsg { 11 | /// name of the derivative token 12 | pub name: String, 13 | /// symbol / ticker of the derivative token 14 | pub symbol: String, 15 | /// decimal places of the derivative token (for UI) 16 | pub decimals: u8, 17 | 18 | /// This is the validator that all tokens will be bonded to 19 | pub validator: String, 20 | /// This is the unbonding period of the native staking module 21 | /// We need this to only allow claims to be redeemed after the money has arrived 22 | pub unbonding_period: Duration, 23 | 24 | /// this is how much the owner takes as a cut when someone unbonds 25 | pub exit_tax: Decimal, 26 | /// This is the minimum amount we will pull out to reinvest, as well as a minimum 27 | /// that can be unbonded (to avoid needless staking tx) 28 | pub min_withdrawal: Uint128, 29 | } 30 | 31 | #[cw_serde] 32 | pub enum ExecuteMsg { 33 | /// Bond will bond all staking tokens sent with the message and release derivative tokens 34 | Bond {}, 35 | /// Unbond will "burn" the given amount of derivative tokens and send the unbonded 36 | /// staking tokens to the message sender (after exit tax is deducted) 37 | Unbond { amount: Uint128 }, 38 | /// Claim is used to claim your native tokens that you previously "unbonded" 39 | /// after the chain-defined waiting period (eg. 3 weeks) 40 | Claim {}, 41 | /// Reinvest will check for all accumulated rewards, withdraw them, and 42 | /// re-bond them to the same validator. Anyone can call this, which updates 43 | /// the value of the token (how much under custody). 44 | Reinvest {}, 45 | /// _BondAllTokens can only be called by the contract itself, after all rewards have been 46 | /// withdrawn. This is an example of using "callbacks" in message flows. 47 | /// This can only be invoked by the contract itself as a return from Reinvest 48 | _BondAllTokens {}, 49 | 50 | /// Implements CW20. Transfer is a base message to move tokens to another account without triggering actions 51 | Transfer { recipient: String, amount: Uint128 }, 52 | /// Implements CW20. Burn is a base message to destroy tokens forever 53 | Burn { amount: Uint128 }, 54 | /// Implements CW20. Send is a base message to transfer tokens to a contract and trigger an action 55 | /// on the receiving contract. 56 | Send { 57 | contract: String, 58 | amount: Uint128, 59 | msg: Binary, 60 | }, 61 | /// Implements CW20 "approval" extension. Allows spender to access an additional amount tokens 62 | /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance 63 | /// expiration with this one. 64 | IncreaseAllowance { 65 | spender: String, 66 | amount: Uint128, 67 | expires: Option, 68 | }, 69 | /// Implements CW20 "approval" extension. Lowers the spender's access of tokens 70 | /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current 71 | /// allowance expiration with this one. 72 | DecreaseAllowance { 73 | spender: String, 74 | amount: Uint128, 75 | expires: Option, 76 | }, 77 | /// Implements CW20 "approval" extension. Transfers amount tokens from owner -> recipient 78 | /// if `env.sender` has sufficient pre-approval. 79 | TransferFrom { 80 | owner: String, 81 | recipient: String, 82 | amount: Uint128, 83 | }, 84 | /// Implements CW20 "approval" extension. Sends amount tokens from owner -> contract 85 | /// if `env.sender` has sufficient pre-approval. 86 | SendFrom { 87 | owner: String, 88 | contract: String, 89 | amount: Uint128, 90 | msg: Binary, 91 | }, 92 | /// Implements CW20 "approval" extension. Destroys tokens forever 93 | BurnFrom { owner: String, amount: Uint128 }, 94 | } 95 | 96 | #[cw_serde] 97 | #[derive(QueryResponses)] 98 | pub enum QueryMsg { 99 | /// Claims shows the number of tokens this address can access when they are done unbonding 100 | #[returns(ClaimsResponse)] 101 | Claims { address: String }, 102 | /// Investment shows metadata on the staking info of the contract 103 | #[returns(InvestmentResponse)] 104 | Investment {}, 105 | 106 | /// Implements CW20. Returns the current balance of the given address, 0 if unset. 107 | #[returns(BalanceResponse)] 108 | Balance { address: String }, 109 | /// Implements CW20. Returns metadata on the contract - name, decimals, supply, etc. 110 | #[returns(TokenInfoResponse)] 111 | TokenInfo {}, 112 | /// Implements CW20 "allowance" extension. 113 | /// Returns how much spender can use from owner account, 0 if unset. 114 | #[returns(AllowanceResponse)] 115 | Allowance { owner: String, spender: String }, 116 | } 117 | 118 | #[cw_serde] 119 | pub struct InvestmentResponse { 120 | pub token_supply: Uint128, 121 | pub staked_tokens: Coin, 122 | // ratio of staked_tokens / token_supply (or how many native tokens that one derivative token is nominally worth) 123 | pub nominal_value: Decimal, 124 | 125 | /// owner created the contract and takes a cut 126 | pub owner: String, 127 | /// this is how much the owner takes as a cut when someone unbonds 128 | pub exit_tax: Decimal, 129 | /// All tokens are bonded to this validator 130 | pub validator: String, 131 | /// This is the minimum amount we will pull out to reinvest, as well as a minimum 132 | /// that can be unbonded (to avoid needless staking tx) 133 | pub min_withdrawal: Uint128, 134 | } 135 | -------------------------------------------------------------------------------- /contracts/cw20-staking/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, Decimal, Uint128}; 3 | use cw_controllers::Claims; 4 | use cw_storage_plus::Item; 5 | use cw_utils::Duration; 6 | 7 | pub const CLAIMS: Claims = Claims::new("claims"); 8 | 9 | /// Investment info is fixed at instantiation, and is used to control the function of the contract 10 | #[cw_serde] 11 | pub struct InvestmentInfo { 12 | /// Owner created the contract and takes a cut 13 | pub owner: Addr, 14 | /// This is the denomination we can stake (and only one we accept for payments) 15 | pub bond_denom: String, 16 | /// This is the unbonding period of the native staking module 17 | /// We need this to only allow claims to be redeemed after the money has arrived 18 | pub unbonding_period: Duration, 19 | /// This is how much the owner takes as a cut when someone unbonds 20 | pub exit_tax: Decimal, 21 | /// All tokens are bonded to this validator 22 | /// FIXME: address validation doesn't work for validator addresses 23 | pub validator: String, 24 | /// This is the minimum amount we will pull out to reinvest, as well as a minimum 25 | /// that can be unbonded (to avoid needless staking tx) 26 | pub min_withdrawal: Uint128, 27 | } 28 | 29 | /// Supply is dynamic and tracks the current supply of staked and ERC20 tokens. 30 | #[cw_serde] 31 | #[derive(Default)] 32 | pub struct Supply { 33 | /// issued is how many derivative tokens this contract has issued 34 | pub issued: Uint128, 35 | /// bonded is how many native tokens exist bonded to the validator 36 | pub bonded: Uint128, 37 | /// claims is how many tokens need to be reserved paying back those who unbonded 38 | pub claims: Uint128, 39 | } 40 | 41 | pub const INVESTMENT: Item = Item::new("invest"); 42 | pub const TOTAL_SUPPLY: Item = Item::new("total_supply"); 43 | -------------------------------------------------------------------------------- /contracts/cw20-streams/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --example schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-streams/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-streams" 3 | version = "0.14.2" 4 | authors = ["Vernon Johnson "] 5 | edition = "2018" 6 | 7 | exclude = [ 8 | # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. 9 | "contract.wasm", 10 | "hash.txt", 11 | ] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [lib] 16 | crate-type = ["cdylib", "rlib"] 17 | 18 | [features] 19 | # for more explicit tests, cargo test --features=backtraces 20 | backtraces = ["cosmwasm-std/backtraces"] 21 | # use library feature to disable all instantiate/execute/query exports 22 | library = [] 23 | 24 | [package.metadata.scripts] 25 | optimize = """docker run --rm -v "$(pwd)":/code \ 26 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ 27 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 28 | cosmwasm/rust-optimizer:0.12.8 29 | """ 30 | 31 | [dependencies] 32 | cosmwasm-std = "1.1.5" 33 | cosmwasm-storage = "1.1.5" 34 | cw-storage-plus = "0.16.0" 35 | cw2 = "0.16.0" 36 | cw20 = "0.16.0" 37 | serde = { version = "1.0.137", default-features = false, features = ["derive"] } 38 | thiserror = "1.0.31" 39 | cosmwasm-schema = "1.1.5" 40 | [dev-dependencies] 41 | 42 | -------------------------------------------------------------------------------- /contracts/cw20-streams/NOTICE: -------------------------------------------------------------------------------- 1 | CW20-Streams: Contract for cw20 token streams 2 | Copyright 2021-22 Vernon Johnson 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /contracts/cw20-streams/README.md: -------------------------------------------------------------------------------- 1 | # CW20 Streams 2 | 3 | This contract enables the creation of cw20 token streams, which allows a cw20 payment to be vested continuously over time. This contract must be instantiated with a cw20 token address, after which any number of payment streams can be created from a single contract instance. 4 | 5 | ## Instantiation 6 | 7 | To instantiate a new instance of this contract you must specify a contract owner, and the cw20 token address used for the streams. Only one cw20 token can be used for payments for each contract instance. 8 | 9 | ## Creating a Stream 10 | A stream can be created using the cw20 [Send / Receive](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md#receiver) flow. This involves triggering a Send message from the cw20 token contract, with a Receive callback that's sent to the token streaming contract. The callback message must include the start time and end time of the stream in seconds, as well as the payment recipient. 11 | 12 | ## Withdrawing payments 13 | Streamed payments can be claimed continously at any point after the start time by triggering a Withdraw message. 14 | 15 | ## Development 16 | ### Compiling 17 | 18 | To generate a development build run: 19 | ``` 20 | cargo build 21 | ``` 22 | 23 | To generate an optimized build run: 24 | 25 | ``` 26 | docker run --rm -v "$(pwd)":/code \ 27 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ 28 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 29 | cosmwasm/rust-optimizer:0.12.4 30 | ``` 31 | 32 | ### Testing 33 | To execute unit tests run: 34 | ``` 35 | cargo test 36 | ``` 37 | 38 | ### Lint 39 | To lint repo run: 40 | ``` 41 | cargo fmt 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /contracts/cw20-streams/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | use cw20_streams::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 3 | 4 | fn main() { 5 | write_api! { 6 | instantiate: InstantiateMsg, 7 | query: QueryMsg, 8 | execute: ExecuteMsg, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/cw20-streams/src/contract.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ContractError; 2 | use crate::msg::{ 3 | ConfigResponse, ExecuteMsg, InstantiateMsg, ListStreamsResponse, QueryMsg, ReceiveMsg, 4 | StreamParams, StreamResponse, 5 | }; 6 | use crate::state::{save_stream, Config, Stream, CONFIG, STREAMS, STREAM_SEQ}; 7 | #[cfg(not(feature = "library"))] 8 | use cosmwasm_std::entry_point; 9 | use cosmwasm_std::{ 10 | from_binary, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, 11 | Uint128, 12 | }; 13 | use cw2::set_contract_version; 14 | use cw20::{Cw20Contract, Cw20ExecuteMsg, Cw20ReceiveMsg}; 15 | use cw_storage_plus::Bound; 16 | 17 | const CONTRACT_NAME: &str = "crates.io:cw20-streams"; 18 | const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); 19 | 20 | #[cfg_attr(not(feature = "library"), entry_point)] 21 | pub fn instantiate( 22 | deps: DepsMut, 23 | _env: Env, 24 | info: MessageInfo, 25 | msg: InstantiateMsg, 26 | ) -> Result { 27 | set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 28 | 29 | let owner = match msg.owner { 30 | Some(own) => deps.api.addr_validate(&own)?, 31 | None => info.sender, 32 | }; 33 | 34 | let config = Config { 35 | owner: owner.clone(), 36 | cw20_addr: deps.api.addr_validate(&msg.cw20_addr)?, 37 | }; 38 | CONFIG.save(deps.storage, &config)?; 39 | 40 | STREAM_SEQ.save(deps.storage, &0u64)?; 41 | 42 | Ok(Response::new() 43 | .add_attribute("method", "instantiate") 44 | .add_attribute("owner", owner) 45 | .add_attribute("cw20_addr", msg.cw20_addr)) 46 | } 47 | 48 | #[cfg_attr(not(feature = "library"), entry_point)] 49 | pub fn execute( 50 | deps: DepsMut, 51 | env: Env, 52 | info: MessageInfo, 53 | msg: ExecuteMsg, 54 | ) -> Result { 55 | match msg { 56 | ExecuteMsg::Receive(msg) => execute_receive(env, deps, info, msg), 57 | ExecuteMsg::Withdraw { id } => execute_withdraw(env, deps, info, id), 58 | } 59 | } 60 | 61 | pub fn execute_create_stream( 62 | env: Env, 63 | deps: DepsMut, 64 | config: Config, 65 | params: StreamParams, 66 | ) -> Result { 67 | let StreamParams { 68 | owner, 69 | recipient, 70 | amount, 71 | start_time, 72 | end_time, 73 | } = params; 74 | let owner = deps.api.addr_validate(&owner)?; 75 | let recipient = deps.api.addr_validate(&recipient)?; 76 | 77 | if start_time > end_time { 78 | return Err(ContractError::InvalidStartTime {}); 79 | } 80 | 81 | let block_time = env.block.time.seconds(); 82 | if start_time <= block_time { 83 | return Err(ContractError::InvalidStartTime {}); 84 | } 85 | 86 | let duration: Uint128 = (end_time - start_time).into(); 87 | 88 | if amount < duration { 89 | return Err(ContractError::AmountLessThanDuration {}); 90 | } 91 | 92 | // Duration must divide evenly into amount, so refund remainder 93 | let refund: u128 = amount 94 | .u128() 95 | .checked_rem(duration.u128()) 96 | .ok_or(ContractError::Overflow {})?; 97 | 98 | let amount = amount - Uint128::new(refund); 99 | 100 | let rate_per_second = amount / duration; 101 | 102 | let stream = Stream { 103 | owner: owner.clone(), 104 | recipient: recipient.clone(), 105 | amount, 106 | claimed_amount: Uint128::zero(), 107 | start_time, 108 | end_time, 109 | rate_per_second, 110 | }; 111 | let id = save_stream(deps, &stream)?; 112 | 113 | let mut response = Response::new() 114 | .add_attribute("method", "create_stream") 115 | .add_attribute("stream_id", id.to_string()) 116 | .add_attribute("owner", owner.clone()) 117 | .add_attribute("recipient", recipient) 118 | .add_attribute("amount", amount) 119 | .add_attribute("start_time", start_time.to_string()) 120 | .add_attribute("end_time", end_time.to_string()); 121 | 122 | if refund > 0 { 123 | let cw20 = Cw20Contract(config.cw20_addr); 124 | let msg = cw20.call(Cw20ExecuteMsg::Transfer { 125 | recipient: owner.into(), 126 | amount: refund.into(), 127 | })?; 128 | 129 | response = response.add_message(msg); 130 | } 131 | Ok(response) 132 | } 133 | 134 | pub fn execute_receive( 135 | env: Env, 136 | deps: DepsMut, 137 | info: MessageInfo, 138 | wrapped: Cw20ReceiveMsg, 139 | ) -> Result { 140 | let config = CONFIG.load(deps.storage)?; 141 | if config.cw20_addr != info.sender { 142 | return Err(ContractError::Unauthorized {}); 143 | } 144 | 145 | let msg: ReceiveMsg = from_binary(&wrapped.msg)?; 146 | match msg { 147 | ReceiveMsg::CreateStream { 148 | start_time, 149 | end_time, 150 | recipient, 151 | } => execute_create_stream( 152 | env, 153 | deps, 154 | config, 155 | StreamParams { 156 | owner: wrapped.sender, 157 | recipient, 158 | amount: wrapped.amount, 159 | start_time, 160 | end_time, 161 | }, 162 | ), 163 | } 164 | } 165 | 166 | pub fn execute_withdraw( 167 | env: Env, 168 | deps: DepsMut, 169 | info: MessageInfo, 170 | id: u64, 171 | ) -> Result { 172 | let mut stream = STREAMS 173 | .may_load(deps.storage, id)? 174 | .ok_or(ContractError::StreamNotFound {})?; 175 | 176 | if stream.recipient != info.sender { 177 | return Err(ContractError::NotStreamRecipient { 178 | recipient: stream.recipient, 179 | }); 180 | } 181 | 182 | if stream.claimed_amount >= stream.amount { 183 | return Err(ContractError::StreamFullyClaimed {}); 184 | } 185 | 186 | let block_time = env.block.time.seconds(); 187 | let time_passed = std::cmp::min(block_time, stream.end_time).saturating_sub(stream.start_time); 188 | let vested = Uint128::from(time_passed) * stream.rate_per_second; 189 | let released = vested - stream.claimed_amount; 190 | 191 | if released.u128() == 0 { 192 | return Err(ContractError::NoFundsToClaim {}); 193 | } 194 | 195 | stream.claimed_amount = vested; 196 | 197 | STREAMS.save(deps.storage, id, &stream)?; 198 | 199 | let config = CONFIG.load(deps.storage)?; 200 | let cw20 = Cw20Contract(config.cw20_addr); 201 | let msg = cw20.call(Cw20ExecuteMsg::Transfer { 202 | recipient: stream.recipient.clone().into(), 203 | amount: released, 204 | })?; 205 | 206 | let res = Response::new() 207 | .add_attribute("method", "withdraw") 208 | .add_attribute("stream_id", id.to_string()) 209 | .add_attribute("amount", released) 210 | .add_attribute("recipient", stream.recipient) 211 | .add_message(msg); 212 | Ok(res) 213 | } 214 | 215 | #[cfg_attr(not(feature = "library"), entry_point)] 216 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { 217 | match msg { 218 | QueryMsg::GetConfig {} => to_binary(&query_config(deps)?), 219 | QueryMsg::GetStream { id } => to_binary(&query_stream(deps, id)?), 220 | QueryMsg::ListStreams { start, limit } => { 221 | to_binary(&query_list_streams(deps, start, limit)?) 222 | } 223 | } 224 | } 225 | 226 | fn query_config(deps: Deps) -> StdResult { 227 | let config = CONFIG.load(deps.storage)?; 228 | Ok(ConfigResponse { 229 | owner: config.owner.into(), 230 | cw20_addr: config.cw20_addr.into(), 231 | }) 232 | } 233 | 234 | fn query_stream(deps: Deps, id: u64) -> StdResult { 235 | let stream = STREAMS.load(deps.storage, id)?; 236 | Ok(StreamResponse { 237 | id, 238 | owner: stream.owner.into(), 239 | recipient: stream.recipient.into(), 240 | amount: stream.amount, 241 | claimed_amount: stream.claimed_amount, 242 | rate_per_second: stream.rate_per_second, 243 | start_time: stream.start_time, 244 | end_time: stream.end_time, 245 | }) 246 | } 247 | 248 | fn query_list_streams( 249 | deps: Deps, 250 | start: Option, 251 | limit: Option, 252 | ) -> StdResult { 253 | let start = start.map(Bound::inclusive); 254 | let limit = limit.unwrap_or(5); 255 | 256 | let streams = STREAMS 257 | .range(deps.storage, start, None, Order::Ascending) 258 | .take(limit.into()) 259 | .map(map_stream) 260 | .collect::>>()?; 261 | Ok(ListStreamsResponse { streams }) 262 | } 263 | 264 | fn map_stream(item: StdResult<(u64, Stream)>) -> StdResult { 265 | item.map(|(id, stream)| StreamResponse { 266 | id, 267 | owner: stream.owner.to_string(), 268 | recipient: stream.recipient.to_string(), 269 | amount: stream.amount, 270 | claimed_amount: stream.claimed_amount, 271 | start_time: stream.start_time, 272 | end_time: stream.end_time, 273 | rate_per_second: stream.rate_per_second, 274 | }) 275 | } 276 | 277 | #[cfg(test)] 278 | mod tests { 279 | use super::*; 280 | use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; 281 | use cosmwasm_std::{Addr, CosmosMsg, WasmMsg}; 282 | 283 | fn get_stream(deps: Deps, id: u64) -> Stream { 284 | let msg = QueryMsg::GetStream { id }; 285 | let res = query(deps, mock_env(), msg).unwrap(); 286 | from_binary(&res).unwrap() 287 | } 288 | 289 | #[test] 290 | fn initialization() { 291 | let mut deps = mock_dependencies(); 292 | let msg = InstantiateMsg { 293 | owner: None, 294 | cw20_addr: String::from("cw20"), 295 | }; 296 | 297 | let info = mock_info("creator", &[]); 298 | instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); 299 | 300 | let msg = QueryMsg::GetConfig {}; 301 | let res = query(deps.as_ref(), mock_env(), msg).unwrap(); 302 | let config: Config = from_binary(&res).unwrap(); 303 | 304 | assert_eq!( 305 | config, 306 | Config { 307 | owner: Addr::unchecked("creator"), 308 | cw20_addr: Addr::unchecked("cw20") 309 | } 310 | ); 311 | } 312 | 313 | #[test] 314 | fn execute_withdraw() { 315 | let mut deps = mock_dependencies(); 316 | let msg = InstantiateMsg { 317 | owner: None, 318 | cw20_addr: String::from("cw20"), 319 | }; 320 | let info = mock_info("cw20", &[]); 321 | instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); 322 | 323 | let sender = Addr::unchecked("alice").to_string(); 324 | let recipient = Addr::unchecked("bob").to_string(); 325 | let amount = Uint128::new(200); 326 | let env = mock_env(); 327 | let start_time = env.block.time.plus_seconds(100).seconds(); 328 | let end_time = env.block.time.plus_seconds(300).seconds(); 329 | 330 | let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { 331 | sender, 332 | amount, 333 | msg: to_binary(&ReceiveMsg::CreateStream { 334 | recipient, 335 | start_time, 336 | end_time, 337 | }) 338 | .unwrap(), 339 | }); 340 | execute(deps.as_mut(), env.clone(), info, msg).unwrap(); 341 | 342 | assert_eq!( 343 | get_stream(deps.as_ref(), 1), 344 | Stream { 345 | owner: Addr::unchecked("alice"), 346 | recipient: Addr::unchecked("bob"), 347 | amount, 348 | claimed_amount: Uint128::new(0), 349 | start_time, 350 | rate_per_second: Uint128::new(1), 351 | end_time 352 | } 353 | ); 354 | 355 | // Stream has not started 356 | let mut info = mock_info("owner", &[]); 357 | info.sender = Addr::unchecked("bob"); 358 | let msg = ExecuteMsg::Withdraw { id: 1 }; 359 | let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); 360 | assert_eq!(err, ContractError::NoFundsToClaim {}); 361 | 362 | // Stream has started so tokens have vested 363 | let msg = ExecuteMsg::Withdraw { id: 1 }; 364 | let mut info = mock_info("owner", &[]); 365 | let mut env = mock_env(); 366 | info.sender = Addr::unchecked("bob"); 367 | env.block.time = env.block.time.plus_seconds(150); 368 | let res = execute(deps.as_mut(), env, info, msg).unwrap(); 369 | let msg = res.messages[0].clone().msg; 370 | 371 | assert_eq!( 372 | msg, 373 | CosmosMsg::Wasm(WasmMsg::Execute { 374 | contract_addr: String::from("cw20"), 375 | msg: to_binary(&Cw20ExecuteMsg::Transfer { 376 | recipient: String::from("bob"), 377 | amount: Uint128::new(50) 378 | }) 379 | .unwrap(), 380 | funds: vec![] 381 | }) 382 | ); 383 | 384 | assert_eq!( 385 | get_stream(deps.as_ref(), 1), 386 | Stream { 387 | owner: Addr::unchecked("alice"), 388 | recipient: Addr::unchecked("bob"), 389 | amount, 390 | claimed_amount: Uint128::new(50), 391 | start_time, 392 | rate_per_second: Uint128::new(1), 393 | end_time 394 | } 395 | ); 396 | 397 | // Stream has ended so claim remaining tokens 398 | 399 | let mut env = mock_env(); 400 | env.block.time = env.block.time.plus_seconds(500); 401 | let mut info = mock_info("owner", &[]); 402 | info.sender = Addr::unchecked("bob"); 403 | let msg = ExecuteMsg::Withdraw { id: 1 }; 404 | let res = execute(deps.as_mut(), env, info, msg).unwrap(); 405 | let msg = res.messages[0].clone().msg; 406 | 407 | assert_eq!( 408 | msg, 409 | CosmosMsg::Wasm(WasmMsg::Execute { 410 | contract_addr: String::from("cw20"), 411 | msg: to_binary(&Cw20ExecuteMsg::Transfer { 412 | recipient: String::from("bob"), 413 | amount: Uint128::new(150) 414 | }) 415 | .unwrap(), 416 | funds: vec![] 417 | }) 418 | ); 419 | } 420 | 421 | #[test] 422 | fn create_stream_with_refund() { 423 | let mut deps = mock_dependencies(); 424 | let msg = InstantiateMsg { 425 | owner: None, 426 | cw20_addr: String::from("cw20"), 427 | }; 428 | let info = mock_info("cw20", &[]); 429 | instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); 430 | 431 | let sender = Addr::unchecked("alice").to_string(); 432 | let recipient = Addr::unchecked("bob").to_string(); 433 | let amount = Uint128::new(350); 434 | let env = mock_env(); 435 | let start_time = env.block.time.plus_seconds(100).seconds(); 436 | let end_time = env.block.time.plus_seconds(400).seconds(); 437 | 438 | let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { 439 | sender, 440 | amount, 441 | msg: to_binary(&ReceiveMsg::CreateStream { 442 | recipient, 443 | start_time, 444 | end_time, 445 | }) 446 | .unwrap(), 447 | }); 448 | 449 | // Make sure remaining funds were refunded if duration didn't divide evenly into amount 450 | let res = execute(deps.as_mut(), env, info, msg).unwrap(); 451 | let refund_msg = res.messages[0].clone().msg; 452 | assert_eq!( 453 | refund_msg, 454 | CosmosMsg::Wasm(WasmMsg::Execute { 455 | contract_addr: String::from("cw20"), 456 | msg: to_binary(&Cw20ExecuteMsg::Transfer { 457 | recipient: String::from("alice"), 458 | amount: Uint128::new(50) 459 | }) 460 | .unwrap(), 461 | funds: vec![] 462 | }) 463 | ); 464 | 465 | assert_eq!( 466 | get_stream(deps.as_ref(), 1), 467 | Stream { 468 | owner: Addr::unchecked("alice"), 469 | recipient: Addr::unchecked("bob"), 470 | amount: Uint128::new(300), // original amount - refund 471 | claimed_amount: Uint128::new(0), 472 | start_time, 473 | rate_per_second: Uint128::new(1), 474 | end_time 475 | } 476 | ); 477 | } 478 | 479 | #[test] 480 | fn invalid_start_time() { 481 | let mut deps = mock_dependencies(); 482 | 483 | let msg = InstantiateMsg { 484 | owner: None, 485 | cw20_addr: String::from("cw20"), 486 | }; 487 | let mut info = mock_info("alice", &[]); 488 | instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); 489 | 490 | let sender = Addr::unchecked("alice").to_string(); 491 | let recipient = Addr::unchecked("bob").to_string(); 492 | let amount = Uint128::new(100); 493 | let start_time = mock_env().block.time.plus_seconds(100).seconds(); 494 | let end_time = mock_env().block.time.plus_seconds(20).seconds(); 495 | 496 | let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { 497 | sender, 498 | amount, 499 | msg: to_binary(&ReceiveMsg::CreateStream { 500 | recipient, 501 | start_time, 502 | end_time, 503 | }) 504 | .unwrap(), 505 | }); 506 | info.sender = Addr::unchecked("cw20"); 507 | let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 508 | assert_eq!(err, ContractError::InvalidStartTime {}); 509 | } 510 | 511 | #[test] 512 | fn invalid_cw20_addr() { 513 | let mut deps = mock_dependencies(); 514 | 515 | let msg = InstantiateMsg { 516 | owner: None, 517 | cw20_addr: String::from("cw20"), 518 | }; 519 | let mut info = mock_info("alice", &[]); 520 | instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); 521 | 522 | let sender = Addr::unchecked("alice").to_string(); 523 | let recipient = Addr::unchecked("bob").to_string(); 524 | let amount = Uint128::new(100); 525 | let start_time = mock_env().block.time.plus_seconds(100).seconds(); 526 | let end_time = mock_env().block.time.plus_seconds(200).seconds(); 527 | 528 | let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { 529 | sender, 530 | amount, 531 | msg: to_binary(&ReceiveMsg::CreateStream { 532 | recipient, 533 | start_time, 534 | end_time, 535 | }) 536 | .unwrap(), 537 | }); 538 | info.sender = Addr::unchecked("wrongCw20"); 539 | let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 540 | assert_eq!(err, ContractError::Unauthorized {}); 541 | } 542 | 543 | #[test] 544 | fn invalid_deposit_amount() { 545 | let mut deps = mock_dependencies(); 546 | 547 | let msg = InstantiateMsg { 548 | owner: None, 549 | cw20_addr: String::from("cw20"), 550 | }; 551 | let mut info = mock_info("alice", &[]); 552 | instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); 553 | 554 | let sender = Addr::unchecked("alice").to_string(); 555 | let recipient = Addr::unchecked("bob").to_string(); 556 | let amount = Uint128::new(3); 557 | let start_time = mock_env().block.time.plus_seconds(100).seconds(); 558 | let end_time = mock_env().block.time.plus_seconds(200).seconds(); 559 | 560 | let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { 561 | sender, 562 | amount, 563 | msg: to_binary(&ReceiveMsg::CreateStream { 564 | recipient, 565 | start_time, 566 | end_time, 567 | }) 568 | .unwrap(), 569 | }); 570 | info.sender = Addr::unchecked("cw20"); 571 | let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 572 | assert_eq!(err, ContractError::AmountLessThanDuration {}); 573 | } 574 | } 575 | -------------------------------------------------------------------------------- /contracts/cw20-streams/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | use cosmwasm_std::Addr; 5 | 6 | #[derive(Error, Debug, PartialEq)] 7 | pub enum ContractError { 8 | #[error("{0}")] 9 | Std(#[from] StdError), 10 | 11 | #[error("Not authorized to perform action")] 12 | Unauthorized {}, 13 | 14 | #[error("The start time is invalid. Start time must be before the end time and after the current block time")] 15 | InvalidStartTime {}, 16 | 17 | #[error("The stream has been fully claimed")] 18 | StreamFullyClaimed {}, 19 | 20 | #[error("The stream can only be claimed by original recipient")] 21 | NotStreamRecipient { recipient: Addr }, 22 | 23 | #[error("No tokens have vested for this stream.")] 24 | NoFundsToClaim {}, 25 | 26 | #[error("Stream does not exist.")] 27 | StreamNotFound {}, 28 | 29 | #[error("Amount must be greater than duration")] 30 | AmountLessThanDuration {}, 31 | 32 | #[error("Stream recipient cannot be the stream owner")] 33 | InvalidRecipient {}, 34 | 35 | #[error("Numerical overflow")] 36 | Overflow {}, 37 | } 38 | -------------------------------------------------------------------------------- /contracts/cw20-streams/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | mod error; 3 | pub mod msg; 4 | pub mod state; 5 | 6 | pub use crate::error::ContractError; 7 | -------------------------------------------------------------------------------- /contracts/cw20-streams/src/msg.rs: -------------------------------------------------------------------------------- 1 | // use crate::state::Stream; 2 | use cosmwasm_schema::{cw_serde, QueryResponses}; 3 | use cosmwasm_std::Uint128; 4 | use cw20::Cw20ReceiveMsg; 5 | 6 | #[cw_serde] 7 | pub struct InstantiateMsg { 8 | pub owner: Option, 9 | pub cw20_addr: String, 10 | } 11 | 12 | #[cw_serde] 13 | pub enum ExecuteMsg { 14 | Receive(Cw20ReceiveMsg), 15 | Withdraw { 16 | id: u64, // Stream id 17 | }, 18 | } 19 | 20 | #[cw_serde] 21 | pub enum ReceiveMsg { 22 | CreateStream { 23 | recipient: String, 24 | start_time: u64, 25 | end_time: u64, 26 | }, 27 | } 28 | 29 | #[cw_serde] 30 | pub struct StreamParams { 31 | pub owner: String, 32 | pub recipient: String, 33 | pub amount: Uint128, 34 | pub start_time: u64, 35 | pub end_time: u64, 36 | } 37 | 38 | #[cw_serde] 39 | #[derive(QueryResponses)] 40 | pub enum QueryMsg { 41 | #[returns(ConfigResponse)] 42 | GetConfig {}, 43 | #[returns(StreamResponse)] 44 | GetStream { id: u64 }, 45 | #[returns(ListStreamsResponse)] 46 | ListStreams { 47 | start: Option, 48 | limit: Option, 49 | }, 50 | } 51 | 52 | #[cw_serde] 53 | pub struct ConfigResponse { 54 | pub owner: String, 55 | pub cw20_addr: String, 56 | } 57 | 58 | #[cw_serde] 59 | pub struct StreamResponse { 60 | pub id: u64, 61 | pub owner: String, 62 | pub recipient: String, 63 | pub amount: Uint128, 64 | pub claimed_amount: Uint128, 65 | pub start_time: u64, 66 | pub end_time: u64, 67 | pub rate_per_second: Uint128, 68 | } 69 | 70 | #[cw_serde] 71 | pub struct ListStreamsResponse { 72 | pub streams: Vec, 73 | } 74 | -------------------------------------------------------------------------------- /contracts/cw20-streams/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, DepsMut, StdResult, Uint128}; 3 | use cw_storage_plus::{Item, Map}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[cw_serde] 7 | pub struct Config { 8 | pub owner: Addr, 9 | pub cw20_addr: Addr, 10 | } 11 | 12 | pub const CONFIG: Item = Item::new("config"); 13 | 14 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 15 | pub struct Stream { 16 | pub owner: Addr, 17 | pub recipient: Addr, 18 | pub amount: Uint128, 19 | pub claimed_amount: Uint128, 20 | pub start_time: u64, 21 | pub end_time: u64, 22 | pub rate_per_second: Uint128, 23 | } 24 | 25 | pub const STREAM_SEQ: Item = Item::new("stream_seq"); 26 | pub const STREAMS: Map = Map::new("stream"); 27 | 28 | pub fn save_stream(deps: DepsMut, stream: &Stream) -> StdResult { 29 | let id = STREAM_SEQ.load(deps.storage)?; 30 | let id = id.checked_add(1).unwrap(); 31 | STREAM_SEQ.save(deps.storage, &id)?; 32 | STREAMS.save(deps.storage, id, stream)?; 33 | Ok(id) 34 | } 35 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | command -v shellcheck > /dev/null && shellcheck "$0" 4 | 5 | # This is imported by cw3-fixed-multisig, which is imported by cw3-flex-multisig 6 | # need to make a separate category to remove race conditions 7 | ALL_CONTRACTS="cw20-atomic-swap cw20-bonding cw20-escrow cw20-staking cw20-merkle-airdrop" 8 | 9 | for cont in $ALL_CONTRACTS; do 10 | ( 11 | cd "contracts/$cont" 12 | echo "Publishing $cont" 13 | cargo publish 14 | ) 15 | done 16 | 17 | echo "Everything is published!" 18 | -------------------------------------------------------------------------------- /scripts/set_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | command -v shellcheck > /dev/null && shellcheck "$0" 4 | 5 | function print_usage() { 6 | echo "Usage: $0 NEW_VERSION" 7 | echo "" 8 | echo "e.g. $0 0.8.0" 9 | } 10 | 11 | if [ "$#" -ne 1 ]; then 12 | print_usage 13 | exit 1 14 | fi 15 | 16 | # Check repo 17 | SCRIPT_DIR="$(realpath "$(dirname "$0")")" 18 | if [[ "$(realpath "$SCRIPT_DIR/..")" != "$(pwd)" ]]; then 19 | echo "Script must be called from the repo root" 20 | exit 2 21 | fi 22 | 23 | # Ensure repo is not dirty 24 | CHANGES_IN_REPO=$(git status --porcelain) 25 | if [[ -n "$CHANGES_IN_REPO" ]]; then 26 | echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" 27 | git status && git --no-pager diff 28 | exit 3 29 | fi 30 | 31 | NEW="$1" 32 | OLD=$(sed -n -e 's/^version[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' packages/cw20/Cargo.toml) 33 | echo "Updating old version $OLD to new version $NEW ..." 34 | 35 | FILES_MODIFIED=() 36 | 37 | for package_dir in packages/*/; do 38 | CARGO_TOML="$package_dir/Cargo.toml" 39 | sed -i -e "s/version[[:space:]]*=[[:space:]]*\"$OLD\"/version = \"$NEW\"/" "$CARGO_TOML" 40 | FILES_MODIFIED+=("$CARGO_TOML") 41 | done 42 | 43 | for contract_dir in contracts/*/; do 44 | CARGO_TOML="$contract_dir/Cargo.toml" 45 | sed -i -e "s/version[[:space:]]*=[[:space:]]*\"$OLD\"/version = \"$NEW\"/" "$CARGO_TOML" 46 | FILES_MODIFIED+=("$CARGO_TOML") 47 | done 48 | 49 | cargo build 50 | FILES_MODIFIED+=("Cargo.lock") 51 | 52 | echo "Staging ${FILES_MODIFIED[*]} ..." 53 | git add "${FILES_MODIFIED[@]}" 54 | git commit -m "Set version: $NEW" 55 | --------------------------------------------------------------------------------