diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..79a7f4345 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ + + +# Description + + + + +## Notable changes + + + +- Lowered gas price on contract handle +- Added new contract query + +## Next steps + + + +- Documentation should be updated for query +- More tests to calculate gas price diff --git a/.gitignore b/.gitignore index 093177bd7..896509b50 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ contracts/hackatom/hash.txt __pycache__/ compiled logs +Cargo.lock diff --git a/.gitmodules b/.gitmodules index 08e301e4b..d30dd8174 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "contracts/snip20"] path = contracts/snip20 - url = git@github.com:enigmampc/snip20-reference-impl.git + url = https://github.com/scrtlabs/snip20-reference-impl.git + branch = master diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..3684d406b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@securesecrets.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index f387badf0..000000000 --- a/Cargo.lock +++ /dev/null @@ -1,1433 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi", -] - -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "backtrace" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bincode2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49f6183038e081170ebbbadee6678966c7d54728938a3e7de7f4e780770318f" -dependencies = [ - "byteorder", - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "cbindgen" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2db2df1ebc842c41fd2c4ae5b5a577faf63bd5151b953db752fc686812bee318" -dependencies = [ - "clap", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn", - "tempfile", - "toml", -] - -[[package]] -name = "cc" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "2.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cosmwasm-schema" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84abfcb5e4909c1f7cf0278a8b506644fa59452933e8c47c8787691afbb5374" -dependencies = [ - "schemars", - "serde_json", -] - -[[package]] -name = "cosmwasm-schema" -version = "0.10.0" -source = "git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print#004c6bca6f2b7f31a6594abe4f44f2e41b1456b3" -dependencies = [ - "schemars", - "serde_json", -] - -[[package]] -name = "cosmwasm-sgx-vm" -version = "0.10.0" -source = "git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.0#490fba9243e6cb291462e9d3c1bcbd1975c0df1e" -dependencies = [ - "base64 0.12.3", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.0)", - "enclave-ffi-types", - "hex", - "lazy_static", - "log", - "memmap", - "parity-wasm", - "schemars", - "serde", - "serde_json", - "sgx_types", - "sgx_urts", - "sha2 0.9.5", - "snafu", -] - -[[package]] -name = "cosmwasm-std" -version = "0.10.0" -source = "git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.0#490fba9243e6cb291462e9d3c1bcbd1975c0df1e" -dependencies = [ - "base64 0.11.0", - "schemars", - "serde", - "serde-json-wasm", - "snafu", -] - -[[package]] -name = "cosmwasm-std" -version = "0.10.0" -source = "git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print#004c6bca6f2b7f31a6594abe4f44f2e41b1456b3" -dependencies = [ - "base64 0.11.0", - "schemars", - "serde", - "serde-json-wasm", - "snafu", -] - -[[package]] -name = "cosmwasm-storage" -version = "0.10.0" -source = "git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print#004c6bca6f2b7f31a6594abe4f44f2e41b1456b3" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "serde", -] - -[[package]] -name = "cpufeatures" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" -dependencies = [ - "libc", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "crypto-mac" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" -dependencies = [ - "generic-array 0.12.4", - "subtle 1.0.0", -] - -[[package]] -name = "derive_more" -version = "0.99.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "difference" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "double" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3b28c212ee85ba07a9323f5363f3f21587a95d1958ea7367a0d913626b5dc4" -dependencies = [ - "float-cmp 0.2.5", - "lazysort", - "maplit", -] - -[[package]] -name = "downcast" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d" - -[[package]] -name = "enclave-ffi-types" -version = "0.1.0" -source = "git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.0#490fba9243e6cb291462e9d3c1bcbd1975c0df1e" -dependencies = [ - "cbindgen", - "derive_more", - "thiserror", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "float-cmp" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08117a43ef6d98f44f38cadd33b996d2322bf460422fceccb4fa03930ca231f0" -dependencies = [ - "num", -] - -[[package]] -name = "float-cmp" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fragile" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2" - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" -dependencies = [ - "crypto-mac", - "digest 0.8.1", -] - -[[package]] -name = "hmac-drbg" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e570451493f10f6581b48cdd530413b63ea9e780f544bfd3bdcaa0d89d1a7b" -dependencies = [ - "digest 0.8.1", - "generic-array 0.12.4", - "hmac", -] - -[[package]] -name = "itoa" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazysort" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e22ff43b231e0e2f87d74984e53ebc73b90ae13397e041214fb07efc64168f" - -[[package]] -name = "libc" -version = "0.2.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" - -[[package]] -name = "libsecp256k1" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc1e2c808481a63dc6da2074752fdd4336a3c8fcc68b83db6f1fd5224ae7962" -dependencies = [ - "arrayref", - "crunchy", - "digest 0.8.1", - "hmac-drbg", - "rand 0.7.3", - "sha2 0.8.2", - "subtle 2.4.1", - "typenum", -] - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "memchr" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" - -[[package]] -name = "memmap" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "micro_mint" -version = "0.1.0" -dependencies = [ - "cosmwasm-schema 0.10.0", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "mockall", - "mockall_double", - "schemars", - "secret-toolkit 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", - "shade-protocol", - "snafu", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg", -] - -[[package]] -name = "mint" -version = "0.1.0" -dependencies = [ - "cosmwasm-schema 0.10.0", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "schemars", - "secret-toolkit 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", - "shade-protocol", - "snafu", -] - -[[package]] -name = "mock_band" -version = "0.1.0" -dependencies = [ - "bincode", - "cosmwasm-schema 0.10.0", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "mockall", - "schemars", - "secret-toolkit 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", - "shade-protocol", - "snafu", -] - -[[package]] -name = "mockall" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab571328afa78ae322493cacca3efac6a0f2e0a67305b4df31fd439ef129ac0" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "lazy_static", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e25b214433f669161f414959594216d8e6ba83b6679d3db96899c0b4639033" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "mockall_double" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e25b78d34b2b713b1d000d629079755cfc166e6a65f9f4c1c012a94305467c5" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "num" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" -dependencies = [ - "num-integer", - "num-iter", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386" -dependencies = [ - "memchr", -] - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "oracle" -version = "0.1.0" -dependencies = [ - "bincode", - "cosmwasm-schema 0.10.0", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "double", - "mockall", - "schemars", - "secret-toolkit 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", - "shade-protocol", - "snafu", -] - -[[package]] -name = "parity-wasm" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc878dac00da22f8f61e7af3157988424567ab01d9920b962ef7dcbd7cd865" - -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" - -[[package]] -name = "predicates" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" -dependencies = [ - "difference", - "float-cmp 0.8.0", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" - -[[package]] -name = "predicates-tree" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dd0fd014130206c9352efbdc92be592751b2b9274dff685348341082c6ea3d" -dependencies = [ - "predicates-core", - "treeline", -] - -[[package]] -name = "proc-macro2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", - "rand_hc 0.3.1", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" - -[[package]] -name = "schemars" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be77ed66abed6954aabf6a3e31a84706bedbf93750d267e92ef4a6d90bbd6a61" -dependencies = [ - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11af7a475c9ee266cfaa9e303a47c830ebe072bf3101ab907a7b7b9d816fa01d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] - -[[package]] -name = "secret-toolkit" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?branch=debug-print#f8de2a926fccab9d1f10653bdbc00ea5436df570" -dependencies = [ - "secret-toolkit-crypto 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "secret-toolkit-serialization 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "secret-toolkit-snip20 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "secret-toolkit-snip721 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "secret-toolkit-storage 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "secret-toolkit-utils 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", -] - -[[package]] -name = "secret-toolkit" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print#41a2eb310b5d1b08ced124a5e3664546bd3b355e" -dependencies = [ - "secret-toolkit-crypto 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "secret-toolkit-serialization 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "secret-toolkit-snip20 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "secret-toolkit-snip721 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "secret-toolkit-storage 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "secret-toolkit-utils 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", -] - -[[package]] -name = "secret-toolkit-crypto" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?branch=debug-print#f8de2a926fccab9d1f10653bdbc00ea5436df570" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "libsecp256k1", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "sha2 0.9.5", -] - -[[package]] -name = "secret-toolkit-crypto" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print#41a2eb310b5d1b08ced124a5e3664546bd3b355e" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "libsecp256k1", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "sha2 0.9.5", -] - -[[package]] -name = "secret-toolkit-serialization" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?branch=debug-print#f8de2a926fccab9d1f10653bdbc00ea5436df570" -dependencies = [ - "bincode2", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-serialization" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print#41a2eb310b5d1b08ced124a5e3664546bd3b355e" -dependencies = [ - "bincode2", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-snip20" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?branch=debug-print#f8de2a926fccab9d1f10653bdbc00ea5436df570" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "schemars", - "secret-toolkit-utils 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-snip20" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print#41a2eb310b5d1b08ced124a5e3664546bd3b355e" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "schemars", - "secret-toolkit-utils 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-snip721" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?branch=debug-print#f8de2a926fccab9d1f10653bdbc00ea5436df570" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "schemars", - "secret-toolkit-utils 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-snip721" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print#41a2eb310b5d1b08ced124a5e3664546bd3b355e" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "schemars", - "secret-toolkit-utils 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-storage" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?branch=debug-print#f8de2a926fccab9d1f10653bdbc00ea5436df570" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "secret-toolkit-serialization 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-storage" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print#41a2eb310b5d1b08ced124a5e3664546bd3b355e" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "secret-toolkit-serialization 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "serde", -] - -[[package]] -name = "secret-toolkit-utils" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?branch=debug-print#f8de2a926fccab9d1f10653bdbc00ea5436df570" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "schemars", - "serde", -] - -[[package]] -name = "secret-toolkit-utils" -version = "0.1.0" -source = "git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print#41a2eb310b5d1b08ced124a5e3664546bd3b355e" -dependencies = [ - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "schemars", - "serde", -] - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - -[[package]] -name = "serde" -version = "1.0.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-json-wasm" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120bad73306616e91acd7ceed522ba96032a51cffeef3cc813de7f367df71e37" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_derive_internals" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sgx_types" -version = "1.1.2" -source = "git+https://github.com/apache/teaclave-sgx-sdk.git?rev=v1.1.2#8f065be7a442157bf16dc7106feb795ea1c578eb" - -[[package]] -name = "sgx_urts" -version = "1.1.2" -source = "git+https://github.com/apache/teaclave-sgx-sdk.git?rev=v1.1.2#8f065be7a442157bf16dc7106feb795ea1c578eb" -dependencies = [ - "libc", - "sgx_types", -] - -[[package]] -name = "sha2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - -[[package]] -name = "sha2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug 0.3.0", -] - -[[package]] -name = "shade-protocol" -version = "0.1.0" -dependencies = [ - "cosmwasm-schema 0.10.0", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "schemars", - "secret-toolkit 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", - "snafu", -] - -[[package]] -name = "snafu" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" -dependencies = [ - "backtrace", - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "snip20-reference-impl" -version = "0.1.0" -dependencies = [ - "base64 0.12.3", - "bincode2", - "cosmwasm-schema 0.9.4", - "cosmwasm-sgx-vm", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "hex", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "schemars", - "secret-toolkit 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?rev=v0.1.1-debug-print)", - "serde", - "sha2 0.9.5", - "snafu", - "subtle 2.4.1", -] - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "subtle" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "tempfile" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" -dependencies = [ - "cfg-if", - "libc", - "rand 0.8.4", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "treasury" -version = "0.1.0" -dependencies = [ - "cosmwasm-schema 0.10.0", - "cosmwasm-std 0.10.0 (git+https://github.com/enigmampc/SecretNetwork?tag=v1.0.4-debug-print)", - "cosmwasm-storage", - "schemars", - "secret-toolkit 0.1.0 (git+https://github.com/enigmampc/secret-toolkit?branch=debug-print)", - "serde", - "shade-protocol", - "snafu", -] - -[[package]] -name = "treeline" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" - -[[package]] -name = "typenum" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" - -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] -name = "unicode-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" - -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index aecb9f1f4..d807e9642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,38 @@ [workspace] members = [ + # Packages + "packages/network_integration", "packages/shade_protocol", + "packages/secretcli", + + # Network setups "contracts/initializer", + "contracts/airdrop", + + # Protocol contracts + "contracts/governance", + "contracts/staking", "contracts/mint", "contracts/micro_mint", "contracts/treasury", "contracts/oracle", "contracts/snip20", + "contracts/scrt_staking", + + # Mock contracts "contracts/mock_band", + + # Tools + "tools/doc2book" ] + +[profile.release] +opt-level = 3 +debug = false +rpath = false +lto = true +debug-assertions = false +codegen-units = 1 +panic = 'abort' +incremental = false +overflow-checks = true diff --git a/README.md b/README.md index 7da7c6c30..ab34db9a0 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,49 @@ # Shade Protocol Core Contracts | Contract | Reference | Description | | --------------------------- | --------------------------------- | ------------------------------------- | -| [`mint`](./contracts/mint) | [doc](./contracts/mint/README.md) | Handles asset burning and silk minting| -| [`oracle`](./contracts/oracle) | [doc](./contracts/oracle/README.md) | Handles asset price queries | -| [`treasury`](./contracts/treasury) | [doc](./contracts/treasury/README.md) | Handles asset price queries | - -## Development +| [`governance`](./contracts/governance) | [doc](./contracts/governance/README.md) | Protocol's governance module | +| [`shade_staking`](./contracts/staking) | [doc](./contracts/staking/README.md) | Snip20 staker | +| [`scrt_staking`](./contracts/scrt_staking) | [doc](./contracts/scrt_staking/README.md) | SCRT staker | +| [`treasury`](./contracts/treasury) | [doc](./contracts/treasury/README.md) | Protocol's asset manager | +| [`mint`](./contracts/micro_mint) | [doc](./contracts/micro_mint/README.md) | Asset burner and minter | +| [`oracle`](./contracts/oracle) | [doc](./contracts/oracle/README.md) | Asset price querier | +| [`airdrop`](./contracts/airdrop) | [doc](./contracts/airdrop/README.md) | Task based, multichain snip20 airdropper | ## Development Environment -Install docker for local environment -Source from [testnet](https://build.scrt.network/dev/quickstart.html#setup-the-local-developer-testnet) +### Environment Setup + +1. Make sure [Docker](https://www.docker.com/) is installed +2. Pull the SN-testnet image +```shell +make server-download ``` -docker run -it --rm -p 26657:26657 -p 26656:26656 -p 1337:1337 -v $(pwd):/root/code --name secretdev enigmampc/secret-network-sw-dev -docker exec -it secretdev /bin/bash +3. Open a terminal inside this repo and run: +```shell +make server-start +``` +4. Inside another terminal run: +```shell +make server-connect ``` + #### Testing the environment -Inside the container: +Inside the container, go to /root/code and compile all the smart contracts: ``` -run python3 contract_tester.py +make ``` - -### Environment Setup - -- Rust v1.44.1+ -- `wasm32-unknown-unknown` target -- Docker -- binaryen - -1. Install `rustup` via https://rustup.rs/ - -2. Run the following: - -```sh -rustup default stable -rustup target add wasm32-unknown-unknown +Then test run all the Protocol unit-tests and integration tests using the [tester](packages/network_integration): +```shell +make integration-tests ``` -3. Make sure [Docker](https://www.docker.com/) is installed - -4. To compile the contracts install binaryen -```sh -apt install binaryen -``` - -### Unit / Integration Tests +### Unit Tests Each contract contains Rust unit and integration tests embedded within the contract source directories. You can run: ```sh cargo unit-test -cargo integration-test -``` - -### Compiling - -Run this script to run all of the contract's unit / integration tests and then prepare the contracts for production in /contracts/compiled: - -```sh -bash ./compile-contracts.sh -``` - -### Testing - -You can optionally run extended tests using the [tester](contracts/compiled/tester.py) - -To run a test deployment on a public testnet you can run ```tester.py --testnet public```. -For the private testnet you can run ```tester.py --testnet private```. +``` \ No newline at end of file diff --git a/contractlib/micro_mintlib.py b/contractlib/micro_mintlib.py index 832c89a64..4b582dd97 100644 --- a/contractlib/micro_mintlib.py +++ b/contractlib/micro_mintlib.py @@ -36,26 +36,6 @@ def __init__(self, label, native_asset, oracle, treasury=None, super().__init__(contract, init_msg, label, admin, uploader, backend, instantiated_contract=instantiated_contract, code_id=code_id) - # def migrate(self, label, code_id, code_hash): - # """ - # Instantiate another mint contract and migrate this contracts info into that one - # :param label: Label name of the contract - # :param code_id: Code id of the contract - # :param code_hash: Code hash - # :return: new Mint - # """ - # msg = json.dumps( - # {"migrate": {"label": label, "code_id": code_id, "code_hash": code_hash}}) - # - # new_mint = copy.deepcopy(self) - # for attribute in self.execute(msg, compute=False)["logs"][0]["events"][0]["attributes"]: - # if attribute["key"] == "contract_address": - # new_mint.address = attribute["value"] - # break - # new_mint.contract_id = code_id - # new_mint.code_hash = code_hash - # return new_mint - def update_config(self, owner=None, native_asset=None, oracle=None): """ Updates the minting contract's config @@ -67,12 +47,14 @@ def update_config(self, owner=None, native_asset=None, oracle=None): raw_msg = {"update_config": {}} if owner is not None: raw_msg["update_config"]["owner"] = owner + if native_asset is not None: contract = { "address": native_asset.address, "code_hash": native_asset.code_hash } raw_msg["update_config"]["native_asset"] = contract + if oracle is not None: contract = { "address": oracle.address, @@ -83,17 +65,17 @@ def update_config(self, owner=None, native_asset=None, oracle=None): msg = json.dumps(raw_msg) return self.execute(msg) - def register_asset(self, snip20, commission=None): + def register_asset(self, snip20, capture=None): """ Registers a SNIP20 asset :param snip20: SNIP20 object to add - :param commission: Comission for the SNIP20 + :param capture: Comission for the SNIP20 :return: Result """ msg = {"register_asset": {"contract": {"address": snip20.address, "code_hash": snip20.code_hash}}} - if commission: - msg['register_asset']['commission'] = str(commission) + if capture: + msg['register_asset']['capture'] = str(capture) return self.execute(json.dumps(msg)) diff --git a/contractlib/oraclelib.py b/contractlib/oraclelib.py index d5e74d5cd..819ec81ec 100644 --- a/contractlib/oraclelib.py +++ b/contractlib/oraclelib.py @@ -23,32 +23,31 @@ def __init__(self, label, band_contract, sscrt, contract='oracle.wasm.gz', admin super().__init__(contract, init_msg, label, admin, uploader, backend, instantiated_contract=instantiated_contract, code_id=code_id) - def get_price(self, symbol): + def price(self, symbol): """ Get current coin price :param symbol: Coin ticker :return: """ - msg = json.dumps({'get_price': {'symbol': symbol}}) - - return self.query(msg) - - def get_prices(self, symbols): - - msg = json.dumps({'get_prices': {'symbols': symbols}}) + msg = json.dumps({'price': {'symbol': symbol}}) return self.query(msg) def register_sswap_pair(self, pair): + msg = json.dumps({'pair': { + 'address': pair.address, + 'code_hash': pair.code_hash, + }}) + return self.execute(msg) + + def register_index(self, symbol, basket: list): msg = json.dumps({ - 'register_sswap_pair': { - 'pair': { - 'address': pair.address, - 'code_hash': pair.code_hash, - } + 'register_index': { + 'symbol': symbol, + 'basket': basket, } }) + print(msg) return self.execute(msg) - diff --git a/contractlib/secretlib/secretlib.py b/contractlib/secretlib/secretlib.py index 5bfbe2dc2..5495b8b0e 100644 --- a/contractlib/secretlib/secretlib.py +++ b/contractlib/secretlib/secretlib.py @@ -3,7 +3,7 @@ import time # Presetup some commands -query_list_code = ['secretcli', 'query', 'compute', 'list-code'] +query_list_code = ['secretd', 'query', 'compute', 'list-code'] MAX_TRIES = 30 GAS_METRICS = [] @@ -18,6 +18,7 @@ def run_command(command): :param wait: Time to wait for command :return: Output string """ + # print('CMD', command) p = Popen(command, stdout=PIPE, stderr=PIPE, text=True) output, err = p.communicate() status = p.wait() @@ -36,7 +37,7 @@ def store_contract(contract, user='a', backend='test'): :return: Contract ID """ - command = ['secretcli', 'tx', 'compute', 'store', f'./compiled/{contract}', + command = ['secretd', 'tx', 'compute', 'store', f'./compiled/{contract}', '--from', user, '--gas', STORE_GAS, '-y'] if backend is not None: @@ -63,7 +64,7 @@ def instantiate_contract(contract, msg, label, user='a', backend='test'): :return: """ - command = ['secretcli', 'tx', 'compute', 'instantiate', contract, msg, '--from', + command = ['secretd', 'tx', 'compute', 'instantiate', contract, msg, '--from', user, '--label', label, '-y', '--gas', '500000'] if backend is not None: @@ -73,19 +74,19 @@ def instantiate_contract(contract, msg, label, user='a', backend='test'): def list_code(): - command = ['secretcli', 'query', 'compute', 'list-code'] + command = ['secretd', 'query', 'compute', 'list-code'] return json.loads(run_command(command)) def list_contract_by_code(code): - command = ['secretcli', 'query', 'compute', 'list-contract-by-code', code] + command = ['secretd', 'query', 'compute', 'list-contract-by-code', code] return json.loads(run_command(command)) def execute_contract(contract, msg, user='a', backend='test', amount=None, compute=True): - command = ['secretcli', 'tx', 'compute', 'execute', contract, msg, '--from', user, '--gas', GAS, '-y'] + command = ['secretd', 'tx', 'compute', 'execute', contract, json.dumps(msg), '--from', user, '--gas', GAS, '-y'] if backend is not None: command += ['--keyring-backend', backend] @@ -100,15 +101,15 @@ def execute_contract(contract, msg, user='a', backend='test', amount=None, compu def query_hash(hash): - return run_command(['secretcli', 'q', 'tx', hash]) + return run_command(['secretd', 'q', 'tx', hash]) def compute_hash(hash): - return run_command(['secretcli', 'q', 'compute', 'tx', hash]) + return run_command(['secretd', 'q', 'compute', 'tx', hash]) def query_contract(contract, msg): - command = ['secretcli', 'query', 'compute', 'query', contract, msg] + command = ['secretd', 'query', 'compute', 'query', contract, json.dumps(msg)] out = run_command(command) try: return json.loads(out) diff --git a/contractlib/snip20lib.py b/contractlib/snip20lib.py index ead3190af..54f8a8946 100644 --- a/contractlib/snip20lib.py +++ b/contractlib/snip20lib.py @@ -1,5 +1,6 @@ from .contractlib import Contract from .secretlib import secretlib +from base64 import b64encode import json @@ -78,7 +79,7 @@ def send(self, account, recipient, amount, message=None): raw_msg = {"send": {"recipient": recipient, "amount": str(amount)}} if message is not None: - raw_msg["send"]["msg"] = message + raw_msg["send"]["msg"] = b64encode(json.dumps(message).encode('utf-8')).decode('utf-8') msg = json.dumps(raw_msg) diff --git a/contracts/airdrop/.cargo/config b/contracts/airdrop/.cargo/config new file mode 100644 index 000000000..c1e7c5086 --- /dev/null +++ b/contracts/airdrop/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" \ No newline at end of file diff --git a/contracts/airdrop/.circleci/config.yml b/contracts/airdrop/.circleci/config.yml new file mode 100644 index 000000000..127e1ae7d --- /dev/null +++ b/contracts/airdrop/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +jobs: + build: + docker: + - image: rust:1.43.1 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + - run: + name: Build + command: cargo wasm --locked + - run: + name: Unit tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Integration tests + command: cargo integration-test --locked + - run: + name: Format source code + command: cargo fmt + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in source code and schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + - target/wasm32-unknown-unknown/release/.fingerprint + - target/wasm32-unknown-unknown/release/build + - target/wasm32-unknown-unknown/release/deps + key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} diff --git a/contracts/airdrop/Cargo.toml b/contracts/airdrop/Cargo.toml new file mode 100644 index 000000000..e2a72726c --- /dev/null +++ b/contracts/airdrop/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "airdrop" +version = "0.1.0" +authors = [ + "Guy Garcia ", +] +edition = "2018" + +exclude = [ + # 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. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +debug-print = ["cosmwasm-std/debug-print"] + +[dependencies] +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } +shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +snafu = { version = "0.6.3" } +rs_merkle = { git = "https://github.com/FloppyDisck/rs-merkle", branch = "node_export" } +mockall = "0.10.2" +mockall_double = "0.2.0" + +[dev-dependencies] +flexible-permits = {git = "https://github.com/securesecrets/flexible-permits", tag = "v1.0.1"} diff --git a/contracts/airdrop/Makefile b/contracts/airdrop/Makefile new file mode 100644 index 000000000..2493c22f4 --- /dev/null +++ b/contracts/airdrop/Makefile @@ -0,0 +1,68 @@ +.PHONY: check +check: + cargo check + +.PHONY: clippy +clippy: + cargo clippy + +PHONY: test +test: unit-test + +.PHONY: unit-test +unit-test: + cargo test + +# This is a local build with debug-prints activated. Debug prints only show up +# in the local development chain (see the `start-server` command below) +# and mainnet won't accept contracts built with the feature enabled. +.PHONY: build _build +build: _build compress-wasm +_build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --features="debug-print" + +# This is a build suitable for uploading to mainnet. +# Calls to `debug_print` get removed by the compiler. +.PHONY: build-mainnet _build-mainnet +build-mainnet: _build-mainnet compress-wasm +_build-mainnet: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +# like build-mainnet, but slower and more deterministic +.PHONY: build-mainnet-reproducible +build-mainnet-reproducible: + docker run --rm -v "$$(pwd)":/contract \ + --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/contract/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + enigmampc/secret-contract-optimizer:1.0.3 + +.PHONY: compress-wasm +compress-wasm: + cp ./target/wasm32-unknown-unknown/release/*.wasm ./contract.wasm + @## The following line is not necessary, may work only on linux (extra size optimization) + @# wasm-opt -Os ./contract.wasm -o ./contract.wasm + cat ./contract.wasm | gzip -9 > ./contract.wasm.gz + +.PHONY: schema +schema: + cargo run --example schema + +# Run local development chain with four funded accounts (named a, b, c, and d) +.PHONY: start-server +start-server: # CTRL+C to stop + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1317:1317 \ + -v $$(pwd):/root/code \ + --name secretdev enigmampc/secret-network-sw-dev:v1.0.4-3 + +# This relies on running `start-server` in another console +# You can run other commands on the secretcli inside the dev image +# by using `docker exec secretdev secretcli`. +.PHONY: store-contract-local +store-contract-local: + docker exec secretdev secretcli tx compute store -y --from a --gas 1000000 /root/code/contract.wasm.gz + +.PHONY: clean +clean: + cargo clean + -rm -f ./contract.wasm ./contract.wasm.gz diff --git a/contracts/airdrop/README.md b/contracts/airdrop/README.md new file mode 100644 index 000000000..619b0784c --- /dev/null +++ b/contracts/airdrop/README.md @@ -0,0 +1,299 @@ + +# Airdrop Contract +* [Introduction](#Introduction) +* [Sections](#Sections) + * [Init](#Init) + * [Admin](#Admin) + * Messages + * [UpdateConfig](#UpdateConfig) + * [AddTasks](#AddTasks) + * [ClaimDecay](#ClaimDecay) + * [Task_Admin](#Task_Admin) + * Messages + * [CompleteTask](#CompleteTask) + * [User](#User) + * Messages + * [CreateAccount](#CreateAccount) + * [UpdateAccount](#UpdateAccount) + * [DisablePermitKey](#DisablePermitKey) + * [Claim](#Claim) + * Queries + * [Config](#Config) + * [Dates](#Dates) + * [TotalClaimed](#TotalClaimed) + * [Account](#Account) + +# Introduction +Contract responsible to handle snip20 airdrop + +# Sections + +## Init +##### Request +|Name |Type |Description | optional | +|--------------|---------------|------------------------------------------------------|----------| +|admin | String | New contract owner; SHOULD be a valid bech32 address | yes | +|dump_address | String | Where the decay amount will be sent | yes | +|airdrop_token | Contract | The token that will be airdropped | no | +|airdrop_amount | String | Total airdrop amount to be claimed | no | +|start_date | u64 | When the airdrop starts in UNIX time | yes | +|end_date | u64 | When the airdrop ends in UNIX time | yes | +|decay_start | u64 | When the airdrop decay starts in UNIX time | yes| +|merkle_root | String | Base 64 encoded merkle root of the airdrop data tree | no | +|total_accounts | String | Total accounts in airdrop (needed for merkle proof) | no | +|max_amount | String | Used to limit the user permit amounts (lowers exploit possibility) | no| +|default_claim | String | The default amount to be gifted regardless of tasks | no | +|task_claim | RequiredTasks | The amounts per tasks to gift | no | +|query_rounding | string | To prevent leaking information, total claimed is rounded off to this value | no + +##Admin + +### Messages + +#### UpdateConfig +Updates the given values +##### Request +|Name |Type |Description | optional | +|--------------|------------|-------------------------------------------------------|----------| +|admin | string | New contract admin; SHOULD be a valid bech32 address | yes | +| dump_address | string | Sets the dump address if there isnt any | yes | +|start_date | u64 | When the airdrop starts in UNIX time | yes | +|end_date | u64 | When the airdrop ends in UNIX time | yes | +|decay_start | u64 | When the airdrop decay starts in UNIX time | yes | +|padding | string | Allows for enforcing constant length messages | yes | + +#### AddTasks +Adds more tasks to complete +##### Request +|Name |Type |Description | optional | +|------|-------|---------------------------|----------| +|tasks | Tasks | The new tasks to be added | no | +|padding | string | Allows for enforcing constant length messages | yes | + +##### Response +```json +{ + "add_tasks": { + "status": "success" + } +} +``` + +#### ClaimDecay +Drains the decayed amount of airdrop into a dump address + +##### Response +```json +{ + "claim_decay": { + "status": "success" + } +} +``` + +##Task Admin + +### Messages + +#### CompleteTask +Complete that address' tasks for a given user +##### Request +|Name |Type |Description | optional | +|--------|--------|-------------------------------------|----------| +|address | String | The address that completed the task | no | +|padding | string | Allows for enforcing constant length messages | yes | + +##### Response +```json +{ + "complete_task": { + "status": "success" + } +} +``` + +##User + +### Messages + +### CreateAccount +Creates an account from which the user will claim all of his given addresses' rewards +##### Request +| Name | Type | Description | optional | +|-----------|----------------------------------|------------------------------------------|----------| +| addresses | Array of [AddressProofPermit](#AddressProofPermit) | Proof that the user owns those addresses | no | +| partial_tree | Array of string | An array of nodes that serve as a proof for the addresses | no | +|padding | string | Allows for enforcing constant length messages | yes | + +##### Response +```json +{ + "create_account": { + "status": "success" + } +} +``` + +### UpdateAccount +Updates a users accounts with more addresses +##### Request +| Name | Type | Description | optional | +|-----------|-----------------------------|------------------------------------------|----------| +| addresses | Array of [AddressProofPermit](#AddressProofPermit) | Proof that the user owns those addresses | no | +| partial_tree | Array of string | An array of nodes that serve as a proof for the addresses | no | +|padding | string | Allows for enforcing constant length messages | yes | + +##### Response +```json +{ + "update_account": { + "status": "success" + } +} +``` + +### DisablePermitKey +Disables that permit's key. Any permit that has that key for that address will be declined. +##### Request +| Name | Type | Description | optional | +|-----------|-----------------------------|------------------------------------------|----------| +| key | string | Permit key | no | +|padding | string | Allows for enforcing constant length messages | yes | + +##### Response +```json +{ + "disable_permit_key": { + "status": "success" + } +} +``` + +#### Claim +Claim the user's available claimable amount + +##### Response +```json +{ + "claim": { + "status": "success" + } +} +``` + +### Queries + +#### GetConfig +Gets the contract's config +#### Response +```json +{ + "config": { + "config": "Contract's config" + } +} +``` + +## Dates +Get the contracts airdrop timeframe, can calculate the decay factor if a time is given +##### Request +|Name |Type |Description | optional | +|--------|--------|-------------------------------------|----------| +|current_date | u64 | The current time in UNIX format | yes | +```json +{ + "dates": { + "start": "Airdrop start", + "end": "Airdrop end", + "decay_start": "Airdrop start of decay", + "decay_factor": "Decay percentage" + } +} +``` + +## TotalClaimed +Shows the total amount of the token that has been claimed. If airdrop hasn't ended then it'll just show an estimation. +##### Request +```json +{ + "total_claimed": { + "claimed": "Claimed amount" + } +} +``` + +## Account +Get the account's information +##### Request +|Name |Type |Description | optional | +|--------|--------|-------------------------------------|----------| +|permit | [AccountProofPermit](#AccountProofMsg)|Address's permit | no | +|current_date | u64 | Current time in UNIT format | yes | +```json +{ + "account": { + "total": "Total airdrop amount", + "claimed": "Claimed amount", + "unclaimed": "Amount available to claim", + "finished_tasks": "All of the finished tasks" + } +} +``` + +## AddressProofPermit +This is a structure used to prove that the user has permission to query that address's information (when querying account info). +This is also used to prove that the user owns that address (when creating/updating accounts) and the given amount is in the airdrop. + +NOTE: The parameters must be in order + +[How to sign](https://github.com/securesecrets/shade/blob/77abdc70bc645d97aee7de5eb9a2347d22da425f/packages/shade_protocol/src/signature/mod.rs#L100) +#### Structure +| Name | Type | Description | optional | +|------------|-----------------|----------------------------------------------------|----------| +| params | AddressProofMsg | Information relevant to the airdrop information | no | +| chain_id | String | Chain ID of the network this proof will be used in | no | +| signature | PermitSignature | Signature of the permit | no | + +## AccountProofMsg +The information inside permits that validate account ownership + +NOTE: The parameters must be in order +### Structure +| Name | Type | Description | optional | +|----------|---------|---------------------------------------------------------|----------| +| contract | String | Airdrop contract | no | +| key | String | Some permit key | no | + + +## AddressProofMsg +The information inside permits that validate the airdrop eligibility and validate the account holder's key. + +NOTE: The parameters must be in order +### Structure +| Name | Type | Description | optional | +|----------|---------|---------------------------------------------------------|----------| +| address | String | Address of the signer (might be redundant) | no | +| amount | String | Airdrop amount | no | +| contract | String | Airdrop contract | no | +| index | Integer | Index of airdrop data in reference to the original tree | no | +| key | String | Some permit key | no | + +## PermitSignature +The signature that proves the validity of the data + +NOTE: The parameters must be in order +### Structure +| Name | Type | Description | optional | +|-----------|--------|---------------------------|----------| +| pub_key | pubkey | Signer's public key | no | +| signature | String | Base 64 encoded signature | no | + +## Pubkey +Public key + +NOTE: The parameters must be in order +### Structure +| Name | Type | Description | optional | +|-------|--------|------------------------------------|----------| +| type | String | Must be tendermint/PubKeySecp256k1 | no | +| value | String | The base 64 key | no | \ No newline at end of file diff --git a/contracts/airdrop/src/contract.rs b/contracts/airdrop/src/contract.rs new file mode 100644 index 000000000..78f2fb1f2 --- /dev/null +++ b/contracts/airdrop/src/contract.rs @@ -0,0 +1,176 @@ +use crate::{ + handle::{ + try_add_tasks, + try_claim, + try_claim_decay, + try_complete_task, + try_create_account, + try_disable_permit_key, + try_update_account, + try_update_config, + }, + query, + state::{config_w, decay_claimed_w, total_claimed_w}, +}; +use cosmwasm_std::{ + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdError, + StdResult, + Storage, + Uint128, +}; +use shade_protocol::airdrop::{claim_info::RequiredTask, Config, HandleMsg, InitMsg, QueryMsg}; +use secret_toolkit::utils::{pad_handle_result, pad_query_result}; + +// Used to pad up responses for better privacy. +pub const RESPONSE_BLOCK_SIZE: usize = 256; + +pub fn init( + deps: &mut Extern, + env: Env, + msg: InitMsg, +) -> StdResult { + // Setup task claim + let mut task_claim = vec![RequiredTask { + address: env.contract.address.clone(), + percent: msg.default_claim, + }]; + let mut claim = msg.task_claim; + task_claim.append(&mut claim); + + // Validate claim percentage + let mut count = Uint128::zero(); + for claim in task_claim.iter() { + count += claim.percent; + } + + if count > Uint128(100) { + return Err(StdError::GenericErr { + msg: "tasks above 100%".to_string(), + backtrace: None, + }); + } + + let start_date = match msg.start_date { + None => env.block.time, + Some(date) => date, + }; + + if let Some(end_date) = msg.end_date { + if end_date < start_date { + return Err(StdError::generic_err( + "Start date must come before end date", + )); + } + } + + // Avoid decay collisions + if let Some(start_decay) = msg.decay_start { + if start_decay < start_date { + return Err(StdError::generic_err( + "Decay cannot start before start date", + )); + } + if let Some(end_date) = msg.end_date { + if start_decay > end_date { + return Err(StdError::generic_err( + "Decay cannot start after the end date", + )); + } + } else { + return Err(StdError::generic_err("Decay must have an end date")); + } + } + + let config = Config { + admin: msg.admin.unwrap_or(env.message.sender), + contract: env.contract.address, + dump_address: msg.dump_address, + airdrop_snip20: msg.airdrop_token.clone(), + airdrop_amount: msg.airdrop_amount, + task_claim, + start_date, + end_date: msg.end_date, + decay_start: msg.decay_start, + merkle_root: msg.merkle_root, + total_accounts: msg.total_accounts, + max_amount: msg.max_amount, + query_rounding: msg.query_rounding, + }; + + config_w(&mut deps.storage).save(&config)?; + + // Initialize claim amount + total_claimed_w(&mut deps.storage).save(&Uint128::zero())?; + + decay_claimed_w(&mut deps.storage).save(&false)?; + + Ok(InitResponse { + messages: vec![], + log: vec![], + }) +} + +pub fn handle( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + pad_handle_result(match msg { + HandleMsg::UpdateConfig { + admin, + dump_address, + query_rounding: redeem_step_size, + start_date, + end_date, + decay_start: start_decay, + .. + } => try_update_config( + deps, + env, + admin, + dump_address, + redeem_step_size, + start_date, + end_date, + start_decay, + ), + HandleMsg::AddTasks { tasks, .. } => try_add_tasks(deps, &env, tasks), + HandleMsg::CompleteTask { address, .. } => try_complete_task(deps, &env, address), + HandleMsg::CreateAccount { + addresses, + partial_tree, + .. + } => try_create_account(deps, &env, addresses, partial_tree), + HandleMsg::UpdateAccount { + addresses, + partial_tree, + .. + } => try_update_account(deps, &env, addresses, partial_tree), + HandleMsg::DisablePermitKey { key, .. } => try_disable_permit_key(deps, &env, key), + HandleMsg::Claim { .. } => try_claim(deps, &env), + HandleMsg::ClaimDecay { .. } => try_claim_decay(deps, &env), + }, RESPONSE_BLOCK_SIZE) +} + +pub fn query( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + pad_query_result(match msg { + QueryMsg::Config {} => to_binary(&query::config(deps)?), + QueryMsg::Dates { current_date } => to_binary(&query::dates(deps, current_date)?), + QueryMsg::TotalClaimed {} => to_binary(&query::total_claimed(deps)?), + QueryMsg::Account { + permit, + current_date, + } => to_binary(&query::account(deps, permit, current_date)?), + }, RESPONSE_BLOCK_SIZE) +} diff --git a/contracts/airdrop/src/handle.rs b/contracts/airdrop/src/handle.rs new file mode 100644 index 000000000..6b1b822bf --- /dev/null +++ b/contracts/airdrop/src/handle.rs @@ -0,0 +1,678 @@ +use crate::state::{ + account_r, + account_total_claimed_w, + account_w, + address_in_account_w, + claim_status_r, + claim_status_w, + config_r, + config_w, + decay_claimed_w, + revoke_permit, + total_claimed_r, + total_claimed_w, + validate_address_permit, +}; +use cosmwasm_std::{ + to_binary, + Api, + Binary, + Decimal, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, + Uint128, +}; +use rs_merkle::{algorithms::Sha256, Hasher, MerkleProof}; +use secret_toolkit::snip20::send_msg; +use shade_protocol::{ + airdrop::{ + account::{Account, AddressProofPermit}, + claim_info::RequiredTask, + Config, + HandleAnswer, + }, + generic_response::ResponseStatus, +}; + +#[allow(clippy::too_many_arguments)] +pub fn try_update_config( + deps: &mut Extern, + env: Env, + admin: Option, + dump_address: Option, + query_rounding: Option, + start_date: Option, + end_date: Option, + decay_start: Option, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + // Check if admin + if env.message.sender != config.admin { + return Err(StdError::unauthorized()); + } + + // Save new info + let mut config = config_w(&mut deps.storage); + config.update(|mut state| { + if let Some(admin) = admin { + state.admin = admin; + } + if let Some(dump_address) = dump_address { + state.dump_address = Some(dump_address); + } + if let Some(query_rounding) = query_rounding { + state.query_rounding = query_rounding; + } + if let Some(start_date) = start_date { + // Avoid date collisions + if let Some(end_date) = end_date { + if start_date > end_date { + return Err(StdError::generic_err( + "New start date is greater than end date", + )); + } + } else if let Some(end_date) = state.end_date { + if start_date > end_date { + return Err(StdError::generic_err( + "New start date is greater than the current end date", + )); + } + } + if let Some(start_decay) = decay_start { + if start_date > start_decay { + return Err(StdError::generic_err( + "New start date is greater than start of decay", + )); + } + } else if let Some(start_decay) = state.decay_start { + if start_date > start_decay { + return Err(StdError::generic_err( + "New start date is greater than the current start of decay", + )); + } + } + + state.start_date = start_date; + } + if let Some(end_date) = end_date { + // Avoid date collisions + if let Some(decay_start) = decay_start { + if decay_start > end_date { + return Err(StdError::generic_err( + "New end date is before start of decay", + )); + } + } else if let Some(decay_start) = state.decay_start { + if decay_start > end_date { + return Err(StdError::generic_err( + "New end date is before current start of decay", + )); + } + } + + state.end_date = Some(end_date); + } + if decay_start.is_some() { + state.decay_start = decay_start + } + + Ok(state) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_add_tasks( + deps: &mut Extern, + env: &Env, + tasks: Vec, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + // Check if admin + if env.message.sender != config.admin { + return Err(StdError::unauthorized()); + } + + config_w(&mut deps.storage).update(|mut config| { + let mut task_list = tasks; + config.task_claim.append(&mut task_list); + + //Validate that they do not exceed 100 + let mut count = Uint128::zero(); + for task in config.task_claim.iter() { + count += task.percent; + } + + if count > Uint128(100) { + return Err(StdError::generic_err("tasks above 100%")); + } + + Ok(config) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddTask { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_create_account( + deps: &mut Extern, + env: &Env, + addresses: Vec, + partial_tree: Vec, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + // Check that airdrop hasn't ended + available(&config, env)?; + + // Check that account doesnt exist + let sender = env.message.sender.to_string(); + if account_r(&deps.storage) + .may_load(sender.as_bytes())? + .is_some() + { + return Err(StdError::generic_err("Account already made")); + } + + let mut account = Account { + addresses: vec![], + total_claimable: Uint128::zero(), + }; + + // Validate permits + try_add_account_addresses( + &mut deps.storage, + &config, + &env.message.sender, + &mut account, + addresses, + partial_tree, + )?; + + // Save account + account_w(&mut deps.storage).save(sender.as_bytes(), &account)?; + + // Add default claim at index 0 + account_total_claimed_w(&mut deps.storage).save(sender.as_bytes(), &Uint128::zero())?; + claim_status_w(&mut deps.storage, 0).save(sender.as_bytes(), &false)?; + + // Claim the airdrop after account creation + let (completed_percentage, unclaimed_percentage) = + update_tasks(&mut deps.storage, &config, sender)?; + let mut messages = vec![]; + // Avoid calculating if theres nothing to claim + if unclaimed_percentage > Uint128::zero() { + let redeem_amount = claim_tokens( + &mut deps.storage, + env, + &config, + &account, + completed_percentage, + unclaimed_percentage, + )?; + + total_claimed_w(&mut deps.storage).update(|claimed| Ok(claimed + redeem_amount))?; + + messages.push(send_msg( + env.message.sender.clone(), + redeem_amount, + None, + None, + None, + 0, + config.airdrop_snip20.code_hash, + config.airdrop_snip20.address, + )?); + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::CreateAccount { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_update_account( + deps: &mut Extern, + env: &Env, + addresses: Vec, + partial_tree: Vec, +) -> StdResult { + // Check if airdrop active + let config = config_r(&deps.storage).load()?; + + // Check that airdrop hasnt ended + available(&config, env)?; + + // Get account + let sender = env.message.sender.clone().to_string(); + let mut account = account_r(&deps.storage).load(sender.as_bytes())?; + + // Run the claim function if theres something to claim + let old_claim_amount = account.total_claimable; + let (completed_percentage, unclaimed_percentage) = + update_tasks(&mut deps.storage, &config, sender.to_string())?; + + let mut redeem_amount = Uint128::zero(); + + if unclaimed_percentage > Uint128::zero() { + redeem_amount = claim_tokens( + &mut deps.storage, + env, + &config, + &account, + completed_percentage, + unclaimed_percentage, + )?; + } + + // Setup the new addresses + try_add_account_addresses( + &mut deps.storage, + &config, + &env.message.sender, + &mut account, + addresses, + partial_tree, + )?; + + let mut messages = vec![]; + if completed_percentage > Uint128::zero() { + // Calculate the total new address amount + let added_address_total = (account.total_claimable - old_claim_amount)?; + account_total_claimed_w(&mut deps.storage).update(sender.as_bytes(), |claimed| { + if let Some(claimed) = claimed { + let new_redeem: Uint128; + if completed_percentage == Uint128(100) { + new_redeem = added_address_total * decay_factor(env.block.time, &config); + } else { + new_redeem = completed_percentage + .multiply_ratio(added_address_total, Uint128(100)) + * decay_factor(env.block.time, &config); + } + + redeem_amount += new_redeem; + Ok(claimed + new_redeem) + } else { + Err(StdError::generic_err("Account total claimed not set")) + } + })?; + + total_claimed_w(&mut deps.storage).update(|claimed| Ok(claimed + redeem_amount))?; + + messages.push(send_msg( + env.message.sender.clone(), + redeem_amount, + None, + None, + None, + 0, + config.airdrop_snip20.code_hash, + config.airdrop_snip20.address, + )?); + } + + account_w(&mut deps.storage).save(sender.as_bytes(), &account)?; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateAccount { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_disable_permit_key( + deps: &mut Extern, + env: &Env, + key: String, +) -> StdResult { + revoke_permit(&mut deps.storage, env.message.sender.to_string(), key); + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::DisablePermitKey { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_complete_task( + deps: &mut Extern, + env: &Env, + account: HumanAddr, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + for (i, task) in config.task_claim.iter().enumerate() { + if task.address == env.message.sender { + claim_status_w(&mut deps.storage, i).update( + account.to_string().as_bytes(), + |status| { + // If there was a state then ignore + if let Some(status) = status { + Ok(status) + } else { + Ok(false) + } + }, + )?; + } + } + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Claim { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_claim( + deps: &mut Extern, + env: &Env, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + // Check that airdrop hasn't ended + available(&config, env)?; + + // Get account + let sender = env.message.sender.clone(); + let account = account_r(&deps.storage).load(sender.to_string().as_bytes())?; + + // Calculate airdrop + let (completed_percentage, unclaimed_percentage) = + update_tasks(&mut deps.storage, &config, sender.to_string())?; + + if unclaimed_percentage == Uint128::zero() { + return Err(StdError::generic_err("No claimable amount available")); + } + + let redeem_amount = claim_tokens( + &mut deps.storage, + env, + &config, + &account, + completed_percentage, + unclaimed_percentage, + )?; + + total_claimed_w(&mut deps.storage).update(|claimed| Ok(claimed + redeem_amount))?; + + Ok(HandleResponse { + messages: vec![send_msg( + sender, + redeem_amount, + None, + None, + None, + 0, + config.airdrop_snip20.code_hash, + config.airdrop_snip20.address, + )?], + log: vec![], + data: Some(to_binary(&HandleAnswer::Claim { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_claim_decay( + deps: &mut Extern, + env: &Env, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + // Check if airdrop ended + if let Some(end_date) = config.end_date { + if let Some(dump_address) = config.dump_address { + if env.block.time > end_date { + decay_claimed_w(&mut deps.storage).update(|claimed| { + if claimed { + Err(StdError::generic_err("Decay already claimed")) + } else { + Ok(true) + } + })?; + + let send_total = + (config.airdrop_amount - total_claimed_r(&deps.storage).load()?)?; + let messages = vec![send_msg( + dump_address, + send_total, + None, + None, + None, + 1, + config.airdrop_snip20.code_hash, + config.airdrop_snip20.address, + )?]; + + return Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::ClaimDecay { + status: ResponseStatus::Success, + })?), + }); + } + } + } + + Err(StdError::unauthorized()) +} + +/// Gets task information and sets them +pub fn update_tasks( + storage: &mut S, + config: &Config, + sender: String, +) -> StdResult<(Uint128, Uint128)> { + // Calculate eligible tasks + let mut completed_percentage = Uint128::zero(); + let mut unclaimed_percentage = Uint128::zero(); + for (index, task) in config.task_claim.iter().enumerate() { + // Check if task has been completed + let state = claim_status_r(storage, index).may_load(sender.as_bytes())?; + + match state { + // Ignore if none + None => {} + Some(claimed) => { + completed_percentage += task.percent; + if !claimed { + // Set claim status to true since we're going to claim it now + claim_status_w(storage, index).save(sender.as_bytes(), &true)?; + + unclaimed_percentage += task.percent; + } + } + } + } + + Ok((completed_percentage, unclaimed_percentage)) +} + +pub fn claim_tokens( + storage: &mut S, + env: &Env, + config: &Config, + account: &Account, + completed_percentage: Uint128, + unclaimed_percentage: Uint128, +) -> StdResult { + // send_amount + let sender = env.message.sender.to_string(); + + // Amount to be redeemed + let mut redeem_amount = Uint128::zero(); + + // Update total claimed and calculate claimable + account_total_claimed_w(storage).update(sender.as_bytes(), |claimed| { + if let Some(claimed) = claimed { + // This solves possible uToken inaccuracies + if completed_percentage == Uint128(100) { + redeem_amount = (account.total_claimable - claimed)?; + } else { + redeem_amount = + unclaimed_percentage.multiply_ratio(account.total_claimable, Uint128(100)); + } + + // Update redeem amount with the decay multiplier + redeem_amount = redeem_amount * decay_factor(env.block.time, config); + + Ok(claimed + redeem_amount) + } else { + Err(StdError::generic_err("Account total claimed not set")) + } + })?; + + Ok(redeem_amount) +} + +/// Validates all of the information and updates relevant states +pub fn try_add_account_addresses( + storage: &mut S, + config: &Config, + sender: &HumanAddr, + account: &mut Account, + addresses: Vec, + partial_tree: Vec, +) -> StdResult<()> { + // Setup the items to validate + let mut leaves_to_validate: Vec<(usize, [u8; 32])> = vec![]; + + // Iterate addresses + for permit in addresses.iter() { + let address: HumanAddr; + // Avoid verifying sender + if &permit.params.address != sender { + // Check permit legitimacy + address = validate_address_permit(storage, permit, config.contract.clone())?; + if address != permit.params.address { + return Err(StdError::generic_err( + "Signer address is not the same as the permit address", + )); + } + } else { + address = sender.clone(); + } + + // Check that airdrop amount does not exceed maximum + if permit.params.amount > config.max_amount { + return Err(StdError::generic_err("Amount exceeds maximum amount")); + } + + // Update address if its not in an account + address_in_account_w(storage).update(address.to_string().as_bytes(), |state| { + if state.is_some() { + return Err(StdError::generic_err(format!( + "{:?} already in an account", + address.to_string() + ))); + } + + Ok(true) + })?; + + // Add account as a leaf + let leaf_hash = + Sha256::hash((address.to_string() + &permit.params.amount.to_string()).as_bytes()); + leaves_to_validate.push((permit.params.index as usize, leaf_hash)); + + // If valid then add to account array and sum total amount + account.addresses.push(address); + account.total_claimable += permit.params.amount; + } + + // Need to sort by index in order for the proof to work + leaves_to_validate.sort_by_key(|item| item.0); + + let mut indices: Vec = vec![]; + let mut leaves: Vec<[u8; 32]> = vec![]; + + for leaf in leaves_to_validate.iter() { + indices.push(leaf.0); + leaves.push(leaf.1); + } + + // Convert partial tree from base64 to binary + let mut partial_tree_binary: Vec<[u8; 32]> = vec![]; + for node in partial_tree.iter() { + let mut arr: [u8; 32] = Default::default(); + arr.clone_from_slice(node.as_slice()); + partial_tree_binary.push(arr); + } + + // Prove that user is in airdrop + let proof = MerkleProof::::new(partial_tree_binary); + // Convert to a fixed length array without messing up the contract + let mut root: [u8; 32] = Default::default(); + root.clone_from_slice(config.merkle_root.as_slice()); + if !proof.verify(root, &indices, &leaves, config.total_accounts as usize) { + return Err(StdError::generic_err("Invalid proof")); + } + + Ok(()) +} + +pub fn available(config: &Config, env: &Env) -> StdResult<()> { + let current_time = env.block.time; + + // Check if airdrop started + if current_time < config.start_date { + return Err(StdError::generic_err(format!( + "Airdrop starts on {}", + config.start_date + ))); + } + if let Some(end_date) = config.end_date { + if current_time > end_date { + return Err(StdError::generic_err(format!( + "Airdrop ended on {}", + end_date + ))); + } + } + + Ok(()) +} + +/// Get the multiplier for decay, will return 1 when decay isnt in effect. +pub fn decay_factor(current_time: u64, config: &Config) -> Decimal { + // Calculate redeem amount after applying decay + if let Some(decay_start) = config.decay_start { + if current_time >= decay_start { + return inverse_normalizer(decay_start, current_time, config.end_date.unwrap()); + } + } + Decimal::one() +} + +/// Get the inverse normalized value [0,1] of x between [min, max] +pub fn inverse_normalizer(min: u64, x: u64, max: u64) -> Decimal { + Decimal::from_ratio(max - x, max - min) +} diff --git a/contracts/airdrop/src/lib.rs b/contracts/airdrop/src/lib.rs new file mode 100644 index 000000000..84be1cef6 --- /dev/null +++ b/contracts/airdrop/src/lib.rs @@ -0,0 +1,49 @@ +pub mod contract; +pub mod handle; +pub mod query; +pub mod state; + +#[cfg(test)] +mod test; + +#[cfg(target_arch = "wasm32")] +mod wasm { + use super::contract; + use cosmwasm_std::{ + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, + }; + + #[no_mangle] + extern "C" fn init(env_ptr: u32, msg_ptr: u32) -> u32 { + do_init( + &contract::init::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn handle(env_ptr: u32, msg_ptr: u32) -> u32 { + do_handle( + &contract::handle::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn query(msg_ptr: u32) -> u32 { + do_query( + &contract::query::, + msg_ptr, + ) + } + + // Other C externs like cosmwasm_vm_version_1, allocate, deallocate are available + // automatically because we `use cosmwasm_std`. +} diff --git a/contracts/airdrop/src/query.rs b/contracts/airdrop/src/query.rs new file mode 100644 index 000000000..e81e19ba5 --- /dev/null +++ b/contracts/airdrop/src/query.rs @@ -0,0 +1,109 @@ +use crate::{ + handle::decay_factor, + state::{ + account_r, + account_total_claimed_r, + claim_status_r, + config_r, + decay_claimed_r, + total_claimed_r, + validate_account_permit, + }, +}; +use cosmwasm_std::{Api, Extern, Querier, StdResult, Storage, Uint128}; +use shade_protocol::{ + airdrop::{account::AccountPermit, claim_info::RequiredTask, QueryAnswer}, + math::{div, mult}, +}; + +pub fn config(deps: &Extern) -> StdResult { + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) +} + +pub fn dates( + deps: &Extern, + current_date: Option, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + Ok(QueryAnswer::Dates { + start: config.start_date, + end: config.end_date, + decay_start: config.decay_start, + decay_factor: current_date.map(|date| Uint128(100) * decay_factor(date, &config)), + }) +} + +pub fn total_claimed( + deps: &Extern, +) -> StdResult { + let claimed: Uint128; + let total_claimed = total_claimed_r(&deps.storage).load()?; + if decay_claimed_r(&deps.storage).load()? { + claimed = total_claimed; + } else { + let config = config_r(&deps.storage).load()?; + claimed = mult( + div(total_claimed, config.query_rounding)?, + config.query_rounding, + ); + } + Ok(QueryAnswer::TotalClaimed { claimed }) +} + +pub fn account( + deps: &Extern, + permit: AccountPermit, + current_date: Option, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + let account_address = validate_account_permit(deps, &permit, config.contract)?; + + let account = account_r(&deps.storage).load(account_address.to_string().as_bytes())?; + + // Calculate eligible tasks + let config = config_r(&deps.storage).load()?; + let mut finished_tasks: Vec = vec![]; + let mut completed_percentage = Uint128::zero(); + let mut unclaimed_percentage = Uint128::zero(); + for (index, task) in config.task_claim.iter().enumerate() { + // Check if task has been completed + let state = claim_status_r(&deps.storage, index) + .may_load(account_address.to_string().as_bytes())?; + + match state { + // Ignore if none + None => {} + Some(claimed) => { + finished_tasks.push(task.clone()); + if !claimed { + unclaimed_percentage += task.percent; + } else { + completed_percentage += task.percent; + } + } + } + } + + let mut unclaimed: Uint128; + + if unclaimed_percentage == Uint128(100) { + unclaimed = account.total_claimable; + } else { + unclaimed = unclaimed_percentage.multiply_ratio(account.total_claimable, Uint128(100)); + } + + if let Some(time) = current_date { + unclaimed = unclaimed * decay_factor(time, &config); + } + + Ok(QueryAnswer::Account { + total: account.total_claimable, + claimed: account_total_claimed_r(&deps.storage) + .load(account_address.to_string().as_bytes())?, + unclaimed, + finished_tasks, + }) +} diff --git a/contracts/airdrop/src/state.rs b/contracts/airdrop/src/state.rs new file mode 100644 index 000000000..28165038f --- /dev/null +++ b/contracts/airdrop/src/state.rs @@ -0,0 +1,169 @@ +use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage, Uint128}; +use cosmwasm_storage::{ + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, +}; +use shade_protocol::airdrop::{ + account::{authenticate_ownership, Account, AccountPermit, AddressProofPermit}, + Config, +}; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static DECAY_CLAIMED_KEY: &[u8] = b"decay_claimed"; +pub static CLAIM_STATUS_KEY: &[u8] = b"claim_status_"; +pub static REWARD_IN_ACCOUNT_KEY: &[u8] = b"reward_in_account"; +pub static ACCOUNTS_KEY: &[u8] = b"accounts"; +pub static TOTAL_CLAIMED_KEY: &[u8] = b"total_claimed"; +pub static USER_TOTAL_CLAIMED_KEY: &[u8] = b"user_total_claimed"; +pub static ACCOUNT_PERMIT_KEY: &str = "account_permit_key"; + +pub fn config_w(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn decay_claimed_w(storage: &mut S) -> Singleton { + singleton(storage, DECAY_CLAIMED_KEY) +} + +pub fn decay_claimed_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, DECAY_CLAIMED_KEY) +} + +// Is address added to an account +pub fn address_in_account_r(storage: &S) -> ReadonlyBucket { + bucket_read(REWARD_IN_ACCOUNT_KEY, storage) +} + +pub fn address_in_account_w(storage: &mut S) -> Bucket { + bucket(REWARD_IN_ACCOUNT_KEY, storage) +} + +// airdrop account +pub fn account_r(storage: &S) -> ReadonlyBucket { + bucket_read(ACCOUNTS_KEY, storage) +} + +pub fn account_w(storage: &mut S) -> Bucket { + bucket(ACCOUNTS_KEY, storage) +} + +// If not found then its unrewarded; if true then claimed +pub fn claim_status_r(storage: &S, index: usize) -> ReadonlyBucket { + let mut key = CLAIM_STATUS_KEY.to_vec(); + key.push(index as u8); + bucket_read(&key, storage) +} + +pub fn claim_status_w(storage: &mut S, index: usize) -> Bucket { + let mut key = CLAIM_STATUS_KEY.to_vec(); + key.push(index as u8); + bucket(&key, storage) +} + +// Total claimed +pub fn total_claimed_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, TOTAL_CLAIMED_KEY) +} + +pub fn total_claimed_w(storage: &mut S) -> Singleton { + singleton(storage, TOTAL_CLAIMED_KEY) +} + +// Total account claimed +pub fn account_total_claimed_r(storage: &S) -> ReadonlyBucket { + bucket_read(USER_TOTAL_CLAIMED_KEY, storage) +} + +pub fn account_total_claimed_w(storage: &mut S) -> Bucket { + bucket(USER_TOTAL_CLAIMED_KEY, storage) +} + +// Account permit key +pub fn account_permit_key_r(storage: &S, account: String) -> ReadonlyBucket { + let key = ACCOUNT_PERMIT_KEY.to_string() + &account; + bucket_read(key.as_bytes(), storage) +} + +pub fn account_permit_key_w(storage: &mut S, account: String) -> Bucket { + let key = ACCOUNT_PERMIT_KEY.to_string() + &account; + bucket(key.as_bytes(), storage) +} + +pub fn revoke_permit(storage: &mut S, account: String, permit_key: String) { + account_permit_key_w(storage, account) + .save(permit_key.as_bytes(), &false) + .unwrap(); +} + +pub fn is_permit_revoked( + storage: &S, + account: String, + permit_key: String, +) -> StdResult { + if account_permit_key_r(storage, account) + .may_load(permit_key.as_bytes())? + .is_some() + { + Ok(true) + } else { + Ok(false) + } +} + +pub fn validate_address_permit( + storage: &S, + permit: &AddressProofPermit, + contract: HumanAddr, +) -> StdResult { + // Check that contract matches + if permit.params.contract != contract { + return Err(StdError::unauthorized()); + } + + // Check that permit is not revoked + if is_permit_revoked( + storage, + permit.params.address.to_string(), + permit.params.key.clone(), + )? { + return Err(StdError::generic_err("permit key revoked")); + } + + // Authenticate permit + authenticate_ownership(permit) +} + +pub fn validate_account_permit( + deps: &Extern, + permit: &AccountPermit, + contract: HumanAddr, +) -> StdResult { + // Check that contract matches + if permit.params.contract != contract { + return Err(StdError::unauthorized()); + } + + // Authenticate permit + let address = permit.validate()?.as_humanaddr(&deps.api)?; + + // Check that permit is not revoked + if is_permit_revoked( + &deps.storage, + address.to_string(), + permit.params.key.clone(), + )? { + return Err(StdError::generic_err("permit key revoked")); + } + + Ok(address) +} diff --git a/contracts/airdrop/src/test.rs b/contracts/airdrop/src/test.rs new file mode 100644 index 000000000..b1a44d35b --- /dev/null +++ b/contracts/airdrop/src/test.rs @@ -0,0 +1,158 @@ +#[cfg(test)] +pub mod tests { + use crate::handle::inverse_normalizer; + use cosmwasm_std::{ + Binary, + HumanAddr, + Uint128, + }; + use flexible_permits::{ + permit::bech32_to_canonical, + transaction::{PermitSignature, PubKey}, + }; + use shade_protocol::{ + airdrop::{ + account::{AddressProofMsg, AddressProofPermit}, + }, + math::{div, mult}, + }; + + #[test] + fn decay_factor() { + assert_eq!( + Uint128(50), + Uint128(100) * inverse_normalizer(100, 200, 300) + ); + + assert_eq!(Uint128(25), Uint128(100) * inverse_normalizer(0, 75, 100)); + } + + #[test] + fn secret_signature() { + let permit = AddressProofPermit { + params: AddressProofMsg{ + address: HumanAddr("secret19q7h2zy8mgesy3r39el5fcm986nxqjd7cgylrz".to_string()), + amount: Uint128(27994412), + contract: HumanAddr("secret17q23878cx2pmjn8cp7sqhylqfpvdw9r8p5q8um".to_string()), + index: 11, + key: "account-creation-permit".to_string() + }, + chain_id: Some("pulsar-2".to_string()), + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64( + "A2uZZ02iy/QhPZ0s6WO8HTEfNZEnt5o5PsQ34WHmQFPK") + .expect("Base 64 invalid") + }, + signature: Binary::from_base64( + "bK+ns5SrA7JeFtHlwt+aLU6wB4hgebTMgdNfTfbRtS8TQx1xztFsLoRKa1rqGKBSobVqftGHuIN0s/6CgY1Gxw==") + .expect("Base 64 invalid") + } + }; + + let permit_addr = permit.validate().expect("Signature validation failed"); + assert_eq!( + permit_addr.as_canonical(), + bech32_to_canonical(permit.params.address.as_str()) + ); + assert_ne!( + permit_addr.as_canonical(), + bech32_to_canonical("secret17q23878cx2pmjn8cp7sqhylqfpvdw9r8p5q8um") + ); + } + + #[test] + fn cosmos_signature() { + let permit = AddressProofPermit { + params: AddressProofMsg{ + address: HumanAddr("cosmos1lj5vh5y8yp4a97jmfwpd98lsg0tf5lsqgnnhq3".to_string()), + amount: Uint128(123752075), + contract: HumanAddr("secret17q23878cx2pmjn8cp7sqhylqfpvdw9r8p5q8um".to_string()), + index: 6, + key: "account-creation-permit".to_string() + }, + chain_id: Some("cosmoshub-4".to_string()), + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64( + "AqcyBLqPn7QnOctkK9i9KhnhD0aHA03+LppvNTCdZ1wK") + .expect("Base 64 invalid") + }, + signature: Binary::from_base64( + "IrJPk51qu1X2w3OvCOgEIdM8zBRi379TAYLLh3aCmB8LbNaFbycgtVwtqa4jGGF2jhnkzZCxObk3Y4OMeId+4A==") + .expect("Base 64 invalid") + } + }; + + let permit_addr = permit.validate().expect("Signature validation failed"); + assert_eq!( + permit_addr.as_canonical(), + bech32_to_canonical(permit.params.address.as_str()) + ); + assert_ne!( + permit_addr.as_canonical(), + bech32_to_canonical("cosmos1ceqk06xpqrq45melc9f8khae0fwaa5y5w0gz6x") + ); + } + + #[test] + fn terra_signature() { + let permit = AddressProofPermit { + params: AddressProofMsg{ + address: HumanAddr("terra1vypeq4lqlsh9k443ghf04uexv9xlzxqlxnrjhl".to_string()), + amount: Uint128(112362871), + contract: HumanAddr("secret17q23878cx2pmjn8cp7sqhylqfpvdw9r8p5q8um".to_string()), + index: 0, + key: "account-creation-permit".to_string() + }, + chain_id: Some("columbus-5".to_string()), + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64( + "A7DF52PBYi26Mgi8zhWCc6IzKTgy/DnNSj5oxlFNT8XU") + .expect("Base 64 invalid") + }, + signature: Binary::from_base64( + "/DFPeocGvP9m/k4h0RxkTQkH5hm7YgjKtBwsly/GdcgN7UPz4ZfZo8xSzhudMbxR1PyQjcNdKLL5IJvYBQCWBQ==") + .expect("Base 64 invalid") + } + }; + + let permit_addr = permit.validate().expect("Signature validation failed"); + assert_eq!( + permit_addr.as_canonical(), + bech32_to_canonical(permit.params.address.as_str()) + ); + assert_ne!( + permit_addr.as_canonical(), + bech32_to_canonical("terra19m2zgdyuq0crpww00jc2a9k70ut944dum53p7x") + ); + } + + #[test] + fn claim_query() { + assert_eq!( + Uint128(300), + mult(div(Uint128(345), Uint128(100)).unwrap(), Uint128(100)) + ) + } + + #[test] + fn claim_query_odd_multiple() { + assert_eq!( + Uint128(13475), + mult(div(Uint128(13480), Uint128(7)).unwrap(), Uint128(7)) + ) + } + + #[test] + fn claim_query_under_step() { + assert_eq!( + Uint128(0), + mult(div(Uint128(200), Uint128(1000)).unwrap(), Uint128(1000)) + ) + } +} diff --git a/contracts/governance/.cargo/config b/contracts/governance/.cargo/config new file mode 100644 index 000000000..882fe08f6 --- /dev/null +++ b/contracts/governance/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/governance/.circleci/config.yml b/contracts/governance/.circleci/config.yml new file mode 100644 index 000000000..127e1ae7d --- /dev/null +++ b/contracts/governance/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +jobs: + build: + docker: + - image: rust:1.43.1 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + - run: + name: Build + command: cargo wasm --locked + - run: + name: Unit tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Integration tests + command: cargo integration-test --locked + - run: + name: Format source code + command: cargo fmt + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in source code and schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + - target/wasm32-unknown-unknown/release/.fingerprint + - target/wasm32-unknown-unknown/release/build + - target/wasm32-unknown-unknown/release/deps + key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} diff --git a/contracts/governance/Cargo.toml b/contracts/governance/Cargo.toml new file mode 100644 index 000000000..fabc0d6c9 --- /dev/null +++ b/contracts/governance/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "governance" +version = "0.1.0" +authors = ["Guy Garcia "] +edition = "2018" + +exclude = [ + # 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. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +debug-print = ["cosmwasm-std/debug-print"] + +[dependencies] +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } +shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +snafu = { version = "0.6.3" } + +[dev-dependencies] +serde_json = { version = "1.0.67"} +mockall = "0.10.2" +mockall_double = "0.2.0" diff --git a/contracts/governance/Makefile b/contracts/governance/Makefile new file mode 100644 index 000000000..2493c22f4 --- /dev/null +++ b/contracts/governance/Makefile @@ -0,0 +1,68 @@ +.PHONY: check +check: + cargo check + +.PHONY: clippy +clippy: + cargo clippy + +PHONY: test +test: unit-test + +.PHONY: unit-test +unit-test: + cargo test + +# This is a local build with debug-prints activated. Debug prints only show up +# in the local development chain (see the `start-server` command below) +# and mainnet won't accept contracts built with the feature enabled. +.PHONY: build _build +build: _build compress-wasm +_build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --features="debug-print" + +# This is a build suitable for uploading to mainnet. +# Calls to `debug_print` get removed by the compiler. +.PHONY: build-mainnet _build-mainnet +build-mainnet: _build-mainnet compress-wasm +_build-mainnet: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +# like build-mainnet, but slower and more deterministic +.PHONY: build-mainnet-reproducible +build-mainnet-reproducible: + docker run --rm -v "$$(pwd)":/contract \ + --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/contract/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + enigmampc/secret-contract-optimizer:1.0.3 + +.PHONY: compress-wasm +compress-wasm: + cp ./target/wasm32-unknown-unknown/release/*.wasm ./contract.wasm + @## The following line is not necessary, may work only on linux (extra size optimization) + @# wasm-opt -Os ./contract.wasm -o ./contract.wasm + cat ./contract.wasm | gzip -9 > ./contract.wasm.gz + +.PHONY: schema +schema: + cargo run --example schema + +# Run local development chain with four funded accounts (named a, b, c, and d) +.PHONY: start-server +start-server: # CTRL+C to stop + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1317:1317 \ + -v $$(pwd):/root/code \ + --name secretdev enigmampc/secret-network-sw-dev:v1.0.4-3 + +# This relies on running `start-server` in another console +# You can run other commands on the secretcli inside the dev image +# by using `docker exec secretdev secretcli`. +.PHONY: store-contract-local +store-contract-local: + docker exec secretdev secretcli tx compute store -y --from a --gas 1000000 /root/code/contract.wasm.gz + +.PHONY: clean +clean: + cargo clean + -rm -f ./contract.wasm ./contract.wasm.gz diff --git a/contracts/governance/src/contract.rs b/contracts/governance/src/contract.rs new file mode 100644 index 000000000..900be82ef --- /dev/null +++ b/contracts/governance/src/contract.rs @@ -0,0 +1,183 @@ +use crate::{ + handle, + proposal_state::total_proposals_w, + query, + state::{admin_commands_list_w, config_w, supported_contracts_list_w}, +}; +use cosmwasm_std::{ + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdResult, + Storage, + Uint128, +}; +use secret_toolkit::snip20::register_receive_msg; +use shade_protocol::governance::{Config, HandleMsg, InitMsg, QueryMsg}; + +pub fn init( + deps: &mut Extern, + env: Env, + msg: InitMsg, +) -> StdResult { + let state = Config { + admin: match msg.admin { + None => env.message.sender.clone(), + Some(admin) => admin, + }, + staker: msg.staker, + funding_token: msg.funding_token.clone(), + funding_amount: msg.funding_amount, + funding_deadline: msg.funding_deadline, + voting_deadline: msg.voting_deadline, + minimum_votes: msg.quorum, + }; + + config_w(&mut deps.storage).save(&state)?; + + // Initialize total proposal counter + total_proposals_w(&mut deps.storage).save(&Uint128(0))?; + + // Initialize lists + admin_commands_list_w(&mut deps.storage).save(&vec![])?; + supported_contracts_list_w(&mut deps.storage).save(&vec![])?; + + Ok(InitResponse { + messages: vec![register_receive_msg( + env.contract_code_hash, + None, + 256, + msg.funding_token.code_hash, + msg.funding_token.address, + )?], + log: vec![], + }) +} + +pub fn handle( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + match msg { + // Proposals + HandleMsg::CreateProposal { + target_contract, + proposal, + description, + } => handle::try_create_proposal( + deps, + &env, + target_contract, + Binary::from(proposal.as_bytes()), + description, + ), + + HandleMsg::Receive { + sender, + amount, + msg, + } => handle::try_fund_proposal(deps, &env, sender, amount, msg), + + // Self interactions + // Config + HandleMsg::UpdateConfig { + admin, + staker, + proposal_deadline, + funding_amount, + funding_deadline, + minimum_votes, + } => handle::try_update_config( + deps, + &env, + admin, + staker, + proposal_deadline, + funding_amount, + funding_deadline, + minimum_votes, + ), + + HandleMsg::DisableStaker {} => handle::try_disable_staker(deps, &env), + + // Supported contract + HandleMsg::AddSupportedContract { name, contract } => { + handle::try_add_supported_contract(deps, &env, name, contract) + } + + HandleMsg::RemoveSupportedContract { name } => { + handle::try_remove_supported_contract(deps, &env, name) + } + + HandleMsg::UpdateSupportedContract { name, contract } => { + handle::try_update_supported_contract(deps, &env, name, contract) + } + + // Admin command + HandleMsg::AddAdminCommand { name, proposal } => { + handle::try_add_admin_command(deps, &env, name, proposal) + } + + HandleMsg::RemoveAdminCommand { name } => { + handle::try_remove_admin_command(deps, &env, name) + } + + HandleMsg::UpdateAdminCommand { name, proposal } => { + handle::try_update_admin_command(deps, &env, name, proposal) + } + + // User interaction + HandleMsg::MakeVote { + voter, + proposal_id, + votes, + } => handle::try_vote(deps, &env, voter, proposal_id, votes), + + HandleMsg::TriggerProposal { proposal_id } => { + handle::try_trigger_proposal(deps, &env, proposal_id) + } + + // Admin interactions + HandleMsg::TriggerAdminCommand { + target, + command, + variables, + description, + } => handle::try_trigger_admin_command(deps, &env, target, command, variables, description), + } +} + +pub fn query( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + match msg { + QueryMsg::GetProposals { start, end, status } => { + to_binary(&query::proposals(deps, start, end, status)?) + } + + QueryMsg::GetProposal { proposal_id } => to_binary(&query::proposal(deps, proposal_id)?), + + QueryMsg::GetTotalProposals {} => to_binary(&query::total_proposals(deps)?), + + QueryMsg::GetProposalVotes { proposal_id } => { + to_binary(&query::proposal_votes(deps, proposal_id)?) + } + + QueryMsg::GetSupportedContracts {} => to_binary(&query::supported_contracts(deps)?), + + QueryMsg::GetSupportedContract { name } => { + to_binary(&query::supported_contract(deps, name)?) + } + + QueryMsg::GetAdminCommands {} => to_binary(&query::admin_commands(deps)?), + + QueryMsg::GetAdminCommand { name } => to_binary(&query::admin_command(deps, name)?), + } +} diff --git a/contracts/governance/src/handle.rs b/contracts/governance/src/handle.rs new file mode 100644 index 000000000..47b3f79c1 --- /dev/null +++ b/contracts/governance/src/handle.rs @@ -0,0 +1,795 @@ +use crate::{ + proposal_state::{ + proposal_funding_batch_w, + proposal_funding_deadline_r, + proposal_funding_deadline_w, + proposal_funding_r, + proposal_funding_w, + proposal_r, + proposal_run_status_w, + proposal_status_r, + proposal_status_w, + proposal_votes_r, + proposal_votes_w, + proposal_voting_deadline_r, + proposal_voting_deadline_w, + proposal_w, + total_proposal_votes_r, + total_proposal_votes_w, + total_proposals_w, + }, + state::{ + admin_commands_list_w, + admin_commands_r, + admin_commands_w, + config_r, + config_w, + supported_contract_r, + supported_contract_w, + supported_contracts_list_w, + }, +}; +use cosmwasm_std::{ + from_binary, + to_binary, + Api, + Binary, + CosmosMsg, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, + Uint128, + WasmMsg, +}; +use secret_toolkit::snip20::{batch_send_msg, send_msg, batch::SendAction}; +use shade_protocol::{ + asset::Contract, + generic_response::{ + ResponseStatus, + ResponseStatus::{Failure, Success}, + }, + governance::{ + proposal::{Proposal, ProposalStatus}, + vote::VoteTally, + AdminCommand, + HandleAnswer, + ADMIN_COMMAND_VARIABLE, + GOVERNANCE_SELF, + }, +}; + +pub fn create_proposal( + deps: &mut Extern, + env: &Env, + target_contract: String, + proposal: Binary, + description: String, +) -> StdResult { + // Check that the target contract is neither the governance or a supported contract + if supported_contract_r(&deps.storage) + .may_load(target_contract.as_bytes())? + .is_none() + && target_contract != *GOVERNANCE_SELF + { + return Err(StdError::NotFound { + kind: "contract is not found".to_string(), + backtrace: None, + }); + } + + // Create new proposal ID + let proposal_id = total_proposals_w(&mut deps.storage).update(|mut id| { + id += Uint128(1); + Ok(id) + })?; + + // Create proposal + let proposal = Proposal { + id: proposal_id, + target: target_contract, + msg: proposal, + description, + }; + + let config = config_r(&deps.storage).load()?; + + // Store the proposal + proposal_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &proposal)?; + // Initialize deadline + proposal_funding_deadline_w(&mut deps.storage).save( + proposal_id.to_string().as_bytes(), + &(env.block.time + config.funding_deadline), + )?; + proposal_status_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &ProposalStatus::Funding)?; + + // Initialize total funding + proposal_funding_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &Uint128::zero())?; + // Initialize the funding batch + proposal_funding_batch_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &vec![])?; + + // Create proposal votes + total_proposal_votes_w(&mut deps.storage).save( + proposal_id.to_string().as_bytes(), + &VoteTally { + yes: Uint128::zero(), + no: Uint128::zero(), + abstain: Uint128::zero(), + }, + )?; + + Ok(proposal_id) +} + +pub fn try_fund_proposal( + deps: &mut Extern, + env: &Env, + sender: HumanAddr, + amount: Uint128, + msg: Option, +) -> StdResult { + let proposal_id: Uint128 = + from_binary(&msg.ok_or_else(|| StdError::not_found("Proposal ID in msg"))?)?; + + // Check if proposal is in funding + let status = proposal_status_r(&deps.storage) + .may_load(proposal_id.to_string().as_bytes())? + .ok_or_else(|| StdError::not_found("Proposal"))?; + if status != ProposalStatus::Funding { + return Err(StdError::unauthorized()); + } + + let mut total = proposal_funding_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; + + let config = config_r(&deps.storage).load()?; + let mut messages = vec![]; + + // Check if deadline is reached + if env.block.time + >= proposal_funding_deadline_r(&deps.storage).load(proposal_id.to_string().as_bytes())? + { + proposal_status_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &ProposalStatus::Expired)?; + + // Send back amount + messages.push(send_msg( + sender, + amount, + None, + None, + None, + 1, + config.funding_token.code_hash.clone(), + config.funding_token.address, + )?); + + // TODO: send total over to treasury + + return Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::FundProposal { + status: Failure, + total_funding: total, + })?), + }); + } + + // Sum amount + total += amount; + + let mut adjusted_amount = amount; + + // return the excess + if total > config.funding_amount { + let excess = (total - config.funding_amount)?; + adjusted_amount = (adjusted_amount - excess)?; + // Set total to max + total = config.funding_amount; + + messages.push(send_msg( + sender.clone(), + excess, + None, + None, + None, + 1, + config.funding_token.code_hash.clone(), + config.funding_token.address.clone(), + )?); + } + + // Update list of people that funded + let amounts = proposal_funding_batch_w(&mut deps.storage).update( + proposal_id.to_string().as_bytes(), + |amounts| { + if let Some(mut amounts) = amounts { + amounts.push(SendAction { + recipient: sender.clone(), + recipient_code_hash: None, + amount: adjusted_amount, + msg: None, + memo: None, + }); + + return Ok(amounts); + } + + Err(StdError::not_found("Funding batch")) + }, + )?; + + // Update proposal status + if total == config.funding_amount { + // Update proposal status + proposal_status_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &ProposalStatus::Voting)?; + // Set vote deadline + proposal_voting_deadline_w(&mut deps.storage).save( + proposal_id.to_string().as_bytes(), + &(env.block.time + config.voting_deadline), + )?; + + // Send back all of the invested prop amount + messages.push(batch_send_msg( + amounts, + None, + 1, + config.funding_token.code_hash, + config.funding_token.address, + )?) + } + + proposal_funding_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &total)?; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::FundProposal { + status: Success, + total_funding: total, + })?), + }) +} + +pub fn try_trigger_proposal( + deps: &mut Extern, + env: &Env, + proposal_id: Uint128, +) -> StdResult { + // Get proposal + let proposal = proposal_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; + let run_status: ResponseStatus; + let mut vote_status = + proposal_status_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; + + // Check if proposal has run + // TODO: This might not be needed + // if proposal_run_status_r(&deps.storage).may_load(proposal_id.to_string().as_bytes())?.is_some() { + // return Err(StdError::generic_err("Proposal has already been executed")) + // } + + // Change proposal behavior according to stake availability + let config = config_r(&deps.storage).load()?; + vote_status = match config.staker { + Some(_) => { + // When staking is enabled funding is required + if vote_status != ProposalStatus::Voting { + return Err(StdError::unauthorized()); + } + + let total_votes = + total_proposal_votes_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; + + // Check if proposal can be run + let voting_deadline = proposal_voting_deadline_r(&deps.storage) + .may_load(proposal_id.to_string().as_bytes())? + .ok_or_else(|| StdError::generic_err("No deadline set"))?; + if voting_deadline > env.block.time { + Err(StdError::unauthorized()) + } else if total_votes.yes + total_votes.no + total_votes.abstain < config.minimum_votes + { + Ok(ProposalStatus::Expired) + } else if total_votes.yes > total_votes.no { + Ok(ProposalStatus::Passed) + } else { + Ok(ProposalStatus::Rejected) + } + } + None => { + // Check if user is an admin in order to trigger the proposal + if config.admin == env.message.sender { + Ok(ProposalStatus::Passed) + } else { + Err(StdError::unauthorized()) + } + } + }?; + + let mut messages: Vec = vec![]; + + let target: Option; + if proposal.target == GOVERNANCE_SELF { + target = Some(Contract { + address: env.contract.address.clone(), + code_hash: env.contract_code_hash.clone(), + }) + } else { + target = supported_contract_r(&deps.storage).may_load(proposal.target.as_bytes())?; + } + + // Check if proposal passed or has a valid target contract + if vote_status != ProposalStatus::Passed { + run_status = Failure; + } else if let Some(target) = target { + run_status = match try_execute_msg(target, proposal.msg) { + Ok(msg) => { + messages.push(msg); + Success + } + Err(_) => Failure, + }; + } else { + run_status = Failure; + } + + // Overwrite + proposal_run_status_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &run_status)?; + proposal_status_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &vote_status)?; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::TriggerProposal { + status: run_status, + })?), + }) +} + +pub fn try_execute_msg(contract: Contract, msg: Binary) -> StdResult { + let execute = WasmMsg::Execute { + msg, + contract_addr: contract.address, + callback_code_hash: contract.code_hash, + send: vec![], + }; + Ok(execute.into()) +} + +pub fn try_vote( + deps: &mut Extern, + env: &Env, + voter: HumanAddr, + proposal_id: Uint128, + votes: VoteTally, +) -> StdResult { + // Check that sender is staking contract and staking is enabled + let config = config_r(&deps.storage).load()?; + if config.staker.is_none() || config.staker.unwrap().address != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Check that proposal is votable + let vote_status = proposal_status_r(&deps.storage) + .may_load(proposal_id.to_string().as_bytes())? + .ok_or_else(|| StdError::not_found("Proposal"))?; + let voting_deadline = proposal_voting_deadline_r(&deps.storage) + .may_load(proposal_id.to_string().as_bytes())? + .ok_or_else(|| StdError::generic_err("No deadline set"))?; + + if vote_status != ProposalStatus::Voting || voting_deadline <= env.block.time { + return Err(StdError::unauthorized()); + } + + // Get proposal voting state + let mut proposal_voting_state = + total_proposal_votes_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; + + // Check if user has already voted + match proposal_votes_r(&deps.storage, proposal_id).may_load(voter.to_string().as_bytes())? { + None => {} + Some(old_votes) => { + // Remove those votes from state + proposal_voting_state.yes = (proposal_voting_state.yes - old_votes.yes)?; + proposal_voting_state.no = (proposal_voting_state.no - old_votes.no)?; + proposal_voting_state.abstain = (proposal_voting_state.abstain - old_votes.abstain)?; + } + } + + // Update state + proposal_voting_state.yes += votes.yes; + proposal_voting_state.no += votes.no; + proposal_voting_state.abstain += votes.abstain; + + // Save staker info + total_proposal_votes_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &proposal_voting_state)?; + proposal_votes_w(&mut deps.storage, proposal_id).save(voter.to_string().as_bytes(), &votes)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::MakeVote { status: Success })?), + }) +} + +pub fn try_trigger_admin_command( + deps: &mut Extern, + env: &Env, + target: String, + command: String, + variables: Vec, + description: String, +) -> StdResult { + // Check that user is admin + if config_r(&deps.storage).load()?.admin != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // First validate that the contract exists + let target_contract = match supported_contract_r(&deps.storage).may_load(target.as_bytes())? { + None => { + return Err(StdError::NotFound { + kind: "Contract not found".to_string(), + backtrace: None, + }); + } + Some(contract) => contract, + }; + + // Check that command exists + let admin_command = match admin_commands_r(&deps.storage).may_load(command.as_bytes())? { + None => { + return Err(StdError::NotFound { + kind: "Command not found".to_string(), + backtrace: None, + }); + } + Some(admin_c) => admin_c, + }; + + // With command validate that number of variables is equal + if admin_command.total_arguments != variables.len() as u16 { + return Err(StdError::GenericErr { + msg: "Variable number doesnt match up".to_string(), + backtrace: None, + }); + } + + // Replace variable spaces + let mut finished_command = admin_command.msg; + for item in variables.iter() { + finished_command = finished_command.replacen(ADMIN_COMMAND_VARIABLE, item, 1); + } + + let mut messages = vec![]; + + // Create new proposal ID + let proposal_id = total_proposals_w(&mut deps.storage).update(|mut id| { + id += Uint128(1); + Ok(id) + })?; + + // Try to run + let proposal = Proposal { + id: proposal_id, + target, + msg: Binary::from(finished_command.as_bytes()), + description, + }; + + // Store the proposal + proposal_w(&mut deps.storage).save(proposal_id.to_string().as_bytes(), &proposal)?; + proposal_funding_deadline_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &env.block.time)?; + proposal_voting_deadline_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &env.block.time)?; + proposal_status_w(&mut deps.storage).save( + proposal_id.to_string().as_bytes(), + &ProposalStatus::AdminRequested, + )?; + let run_status = + match try_execute_msg(target_contract, Binary::from(finished_command.as_bytes())) { + Ok(executed_msg) => { + messages.push(executed_msg); + Success + } + Err(_) => Failure, + }; + proposal_run_status_w(&mut deps.storage) + .save(proposal_id.to_string().as_bytes(), &run_status)?; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::TriggerAdminCommand { + status: run_status, + proposal_id, + })?), + }) +} + +/// SELF only interactions + +pub fn try_create_proposal( + deps: &mut Extern, + env: &Env, + target_contract: String, + proposal: Binary, + description: String, +) -> StdResult { + let proposal_id = create_proposal(deps, env, target_contract, proposal, description)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::CreateProposal { + status: Success, + proposal_id, + })?), + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn try_update_config( + deps: &mut Extern, + env: &Env, + admin: Option, + staker: Option, + proposal_deadline: Option, + funding_amount: Option, + funding_deadline: Option, + minimum_votes: Option, +) -> StdResult { + // It has to be self + if env.contract.address != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + config_w(&mut deps.storage).update(|mut state| { + if let Some(admin) = admin { + state.admin = admin; + } + if staker.is_some() { + state.staker = staker; + } + if let Some(proposal_deadline) = proposal_deadline { + state.voting_deadline = proposal_deadline; + } + if let Some(funding_amount) = funding_amount { + state.funding_amount = funding_amount; + } + if let Some(funding_deadline) = funding_deadline { + state.funding_deadline = funding_deadline; + } + if let Some(minimum_votes) = minimum_votes { + state.minimum_votes = minimum_votes; + } + + Ok(state) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateConfig { status: Success })?), + }) +} + +pub fn try_disable_staker( + deps: &mut Extern, + _env: &Env, +) -> StdResult { + config_w(&mut deps.storage).update(|mut state| { + state.staker = None; + Ok(state) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::DisableStaker { status: Success })?), + }) +} + +pub fn try_add_supported_contract( + deps: &mut Extern, + env: &Env, + name: String, + contract: Contract, +) -> StdResult { + // It has to be self + if env.contract.address != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Cannot be the same name as governance default + if name == *GOVERNANCE_SELF { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Supported contract cannot exist + if supported_contract_r(&deps.storage) + .may_load(name.as_bytes())? + .is_some() + { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Save contract + supported_contract_w(&mut deps.storage).save(name.as_bytes(), &contract)?; + + // Update command list + supported_contracts_list_w(&mut deps.storage).update(|mut arr| { + arr.push(name); + Ok(arr) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddSupportedContract { + status: Success, + })?), + }) +} + +pub fn try_remove_supported_contract( + deps: &mut Extern, + env: &Env, + name: String, +) -> StdResult { + // It has to be self + if env.contract.address != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Cannot be the same name as governance default + if name == *GOVERNANCE_SELF { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Remove contract + supported_contract_w(&mut deps.storage).remove(name.as_bytes()); + + // Remove from array + supported_contracts_list_w(&mut deps.storage).update(|mut arr| { + arr.retain(|value| *value != name); + Ok(arr) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::RemoveSupportedContract { + status: Success, + })?), + }) +} + +pub fn try_update_supported_contract( + deps: &mut Extern, + env: &Env, + name: String, + contract: Contract, +) -> StdResult { + // It has to be self and cannot be the same name as governance default + if env.contract.address != env.message.sender || name == *GOVERNANCE_SELF { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Replace contract + supported_contract_w(&mut deps.storage).update(name.as_bytes(), |_state| Ok(contract))?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateSupportedContract { + status: Success, + })?), + }) +} + +pub fn try_add_admin_command( + deps: &mut Extern, + env: &Env, + name: String, + proposal: String, +) -> StdResult { + // It has to be self + if env.contract.address != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Admin command cannot exist + if admin_commands_r(&deps.storage) + .may_load(name.as_bytes())? + .is_some() + { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Save command + admin_commands_w(&mut deps.storage).save(name.as_bytes(), &AdminCommand { + msg: proposal.clone(), + total_arguments: proposal.matches(ADMIN_COMMAND_VARIABLE).count() as u16, + })?; + + // Update command list + admin_commands_list_w(&mut deps.storage).update(|mut arr| { + arr.push(name); + Ok(arr) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddAdminCommand { + status: Success, + })?), + }) +} + +pub fn try_remove_admin_command( + deps: &mut Extern, + env: &Env, + name: String, +) -> StdResult { + // It has to be self + if env.contract.address != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Remove command + admin_commands_w(&mut deps.storage).remove(name.as_bytes()); + + // Remove from array + admin_commands_list_w(&mut deps.storage).update(|mut arr| { + arr.retain(|value| *value != name); + Ok(arr) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::RemoveAdminCommand { + status: Success, + })?), + }) +} + +pub fn try_update_admin_command( + deps: &mut Extern, + env: &Env, + name: String, + proposal: String, +) -> StdResult { + // It has to be self + if env.contract.address != env.message.sender { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Replace contract + admin_commands_w(&mut deps.storage).update(name.as_bytes(), |_state| { + Ok(AdminCommand { + msg: proposal.clone(), + total_arguments: proposal.matches(ADMIN_COMMAND_VARIABLE).count() as u16, + }) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateAdminCommand { + status: Success, + })?), + }) +} diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs new file mode 100644 index 000000000..d82e1f663 --- /dev/null +++ b/contracts/governance/src/lib.rs @@ -0,0 +1,50 @@ +pub mod contract; +pub mod handle; +pub mod proposal_state; +pub mod query; +pub mod state; + +#[cfg(test)] +mod test; + +#[cfg(target_arch = "wasm32")] +mod wasm { + use super::contract; + use cosmwasm_std::{ + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, + }; + + #[no_mangle] + extern "C" fn init(env_ptr: u32, msg_ptr: u32) -> u32 { + do_init( + &contract::init::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn handle(env_ptr: u32, msg_ptr: u32) -> u32 { + do_handle( + &contract::handle::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn query(msg_ptr: u32) -> u32 { + do_query( + &contract::query::, + msg_ptr, + ) + } + + // Other C externs like cosmwasm_vm_version_1, allocate, deallocate are available + // automatically because we `use cosmwasm_std`. +} diff --git a/contracts/governance/src/proposal_state.rs b/contracts/governance/src/proposal_state.rs new file mode 100644 index 000000000..9c20ddbbf --- /dev/null +++ b/contracts/governance/src/proposal_state.rs @@ -0,0 +1,130 @@ +use cosmwasm_std::{Storage, Uint128}; +use cosmwasm_storage::{ + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, +}; +use secret_toolkit::snip20::batch::SendAction; +use shade_protocol::{ + generic_response::ResponseStatus, + governance::{ + proposal::{Proposal, ProposalStatus}, + vote::VoteTally, + }, +}; + +// Proposals +pub static PROPOSAL_KEY: &[u8] = b"proposals"; +pub static PROPOSAL_VOTE_DEADLINE_KEY: &[u8] = b"proposal_vote_deadline_key"; +pub static PROPOSAL_FUNDING_DEADLINE_KEY: &[u8] = b"proposal_funding_deadline_key"; +pub static PROPOSAL_STATUS_KEY: &[u8] = b"proposal_status_key"; +pub static PROPOSAL_RUN_KEY: &[u8] = b"proposal_run_key"; +pub static PROPOSAL_FUNDING_KEY: &[u8] = b"proposal_funding_key"; +pub static PROPOSAL_FUNDING_BATCH_KEY: &[u8] = b"proposal_funding_batch_key"; +pub static PROPOSAL_VOTES_KEY: &str = "proposal_votes"; +pub static TOTAL_PROPOSAL_VOTES_KEY: &[u8] = b"total_proposal_votes"; +pub static TOTAL_PROPOSAL_KEY: &[u8] = b"total_proposals"; + +// Total proposal counter +pub fn total_proposals_w(storage: &mut S) -> Singleton { + singleton(storage, TOTAL_PROPOSAL_KEY) +} + +pub fn total_proposals_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, TOTAL_PROPOSAL_KEY) +} + +// Individual proposals +pub fn proposal_r(storage: &S) -> ReadonlyBucket { + bucket_read(PROPOSAL_KEY, storage) +} + +pub fn proposal_w(storage: &mut S) -> Bucket { + bucket(PROPOSAL_KEY, storage) +} + +// Proposal funding deadline +pub fn proposal_funding_deadline_r(storage: &S) -> ReadonlyBucket { + bucket_read(PROPOSAL_FUNDING_DEADLINE_KEY, storage) +} + +pub fn proposal_funding_deadline_w(storage: &mut S) -> Bucket { + bucket(PROPOSAL_FUNDING_DEADLINE_KEY, storage) +} + +// Proposal voting deadline +pub fn proposal_voting_deadline_r(storage: &S) -> ReadonlyBucket { + bucket_read(PROPOSAL_VOTE_DEADLINE_KEY, storage) +} + +pub fn proposal_voting_deadline_w(storage: &mut S) -> Bucket { + bucket(PROPOSAL_VOTE_DEADLINE_KEY, storage) +} + +// Proposal status +pub fn proposal_status_r(storage: &S) -> ReadonlyBucket { + bucket_read(PROPOSAL_STATUS_KEY, storage) +} + +pub fn proposal_status_w(storage: &mut S) -> Bucket { + bucket(PROPOSAL_STATUS_KEY, storage) +} + +// Proposal total funding +pub fn proposal_funding_r(storage: &S) -> ReadonlyBucket { + bucket_read(PROPOSAL_FUNDING_KEY, storage) +} + +pub fn proposal_funding_w(storage: &mut S) -> Bucket { + bucket(PROPOSAL_FUNDING_KEY, storage) +} + +// Proposal funding batch +pub fn proposal_funding_batch_r(storage: &S) -> ReadonlyBucket> { + bucket_read(PROPOSAL_FUNDING_BATCH_KEY, storage) +} + +pub fn proposal_funding_batch_w(storage: &mut S) -> Bucket> { + bucket(PROPOSAL_FUNDING_BATCH_KEY, storage) +} + +// Proposal run status - will be available after proposal is run +pub fn proposal_run_status_r(storage: &S) -> ReadonlyBucket { + bucket_read(PROPOSAL_RUN_KEY, storage) +} + +pub fn proposal_run_status_w(storage: &mut S) -> Bucket { + bucket(PROPOSAL_RUN_KEY, storage) +} + +// Individual proposal user votes +pub fn proposal_votes_r( + storage: &S, + proposal: Uint128, +) -> ReadonlyBucket { + bucket_read( + (proposal.to_string() + PROPOSAL_VOTES_KEY).as_bytes(), + storage, + ) +} + +pub fn proposal_votes_w(storage: &mut S, proposal: Uint128) -> Bucket { + bucket( + (proposal.to_string() + PROPOSAL_VOTES_KEY).as_bytes(), + storage, + ) +} + +// Total proposal votes +pub fn total_proposal_votes_r(storage: &S) -> ReadonlyBucket { + bucket_read(TOTAL_PROPOSAL_VOTES_KEY, storage) +} + +pub fn total_proposal_votes_w(storage: &mut S) -> Bucket { + bucket(TOTAL_PROPOSAL_VOTES_KEY, storage) +} diff --git a/contracts/governance/src/query.rs b/contracts/governance/src/query.rs new file mode 100644 index 000000000..7572db8cc --- /dev/null +++ b/contracts/governance/src/query.rs @@ -0,0 +1,141 @@ +use cosmwasm_std::{Api, Extern, Querier, StdError, StdResult, Storage, Uint128}; +use shade_protocol::governance::{ + proposal::{ProposalStatus, QueriedProposal}, + QueryAnswer, +}; + +use crate::{ + proposal_state::{ + proposal_funding_deadline_r, + proposal_funding_r, + proposal_r, + proposal_run_status_r, + proposal_status_r, + proposal_voting_deadline_r, + total_proposal_votes_r, + total_proposals_r, + }, + state::{ + admin_commands_list_r, + admin_commands_r, + supported_contract_r, + supported_contracts_list_r, + }, +}; + +fn build_proposal( + deps: &Extern, + proposal_id: Uint128, +) -> StdResult { + let proposal = proposal_r(&deps.storage).load(proposal_id.to_string().as_bytes())?; + + Ok(QueriedProposal { + id: proposal.id, + target: proposal.target, + msg: proposal.msg, + description: proposal.description, + funding_deadline: proposal_funding_deadline_r(&deps.storage) + .load(proposal_id.to_string().as_bytes())?, + voting_deadline: proposal_voting_deadline_r(&deps.storage) + .may_load(proposal_id.to_string().as_bytes())?, + total_funding: proposal_funding_r(&deps.storage) + .load(proposal_id.to_string().as_bytes())?, + status: proposal_status_r(&deps.storage).load(proposal_id.to_string().as_bytes())?, + run_status: proposal_run_status_r(&deps.storage) + .may_load(proposal_id.to_string().as_bytes())?, + }) +} + +pub fn proposals( + deps: &Extern, + start: Uint128, + end: Uint128, + status: Option, +) -> StdResult { + let mut proposals: Vec = vec![]; + + let max = total_proposals_r(&deps.storage).load()?; + + if start > max { + return Err(StdError::NotFound { + kind: "Proposal doesnt exist".to_string(), + backtrace: None, + }); + } + + let clamped_start = start.max(Uint128(1)); + + for i in clamped_start.u128()..((end + clamped_start).min(max).u128() + 1) { + let proposal = build_proposal(deps, Uint128(i))?; + + // Filter proposal by status if it was specified in fn params. + if let Some(s) = &status { + if s != &proposal.status { + continue; + } + } + proposals.push(proposal) + } + + Ok(QueryAnswer::Proposals { proposals }) +} + +pub fn proposal( + deps: &Extern, + proposal_id: Uint128, +) -> StdResult { + Ok(QueryAnswer::Proposal { + proposal: build_proposal(deps, proposal_id)?, + }) +} + +pub fn total_proposals( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::TotalProposals { + total: total_proposals_r(&deps.storage).load()?, + }) +} + +pub fn proposal_votes( + deps: &Extern, + proposal_id: Uint128, +) -> StdResult { + Ok(QueryAnswer::ProposalVotes { + status: total_proposal_votes_r(&deps.storage).load(proposal_id.to_string().as_bytes())?, + }) +} + +pub fn supported_contracts( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::SupportedContracts { + contracts: supported_contracts_list_r(&deps.storage).load()?, + }) +} + +pub fn supported_contract( + deps: &Extern, + name: String, +) -> StdResult { + Ok(QueryAnswer::SupportedContract { + contract: supported_contract_r(&deps.storage).load(name.as_bytes())?, + }) +} + +pub fn admin_commands( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::AdminCommands { + commands: admin_commands_list_r(&deps.storage).load()?, + }) +} + +pub fn admin_command( + deps: &Extern, + name: String, +) -> StdResult { + Ok(QueryAnswer::AdminCommand { + command: admin_commands_r(&deps.storage).load(name.as_bytes())?, + }) +} diff --git a/contracts/governance/src/state.rs b/contracts/governance/src/state.rs new file mode 100644 index 000000000..79875edc3 --- /dev/null +++ b/contracts/governance/src/state.rs @@ -0,0 +1,67 @@ +use cosmwasm_std::Storage; +use cosmwasm_storage::{ + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, +}; +use shade_protocol::{ + asset::Contract, + governance::{AdminCommand, Config}, +}; + +pub static CONFIG_KEY: &[u8] = b"config"; +// Saved contracts +pub static CONTRACT_KEY: &[u8] = b"supported_contracts"; +pub static CONTRACT_LIST_KEY: &[u8] = b"supported_contracts_list"; +// Admin commands +pub static ADMIN_COMMANDS_KEY: &[u8] = b"admin_commands"; +pub static ADMIN_COMMANDS_LIST_KEY: &[u8] = b"admin_commands_list"; + +pub fn config_w(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +// Supported contracts + +pub fn supported_contract_r(storage: &S) -> ReadonlyBucket { + bucket_read(CONTRACT_KEY, storage) +} + +pub fn supported_contract_w(storage: &mut S) -> Bucket { + bucket(CONTRACT_KEY, storage) +} + +pub fn supported_contracts_list_w(storage: &mut S) -> Singleton> { + singleton(storage, CONTRACT_LIST_KEY) +} + +pub fn supported_contracts_list_r(storage: &S) -> ReadonlySingleton> { + singleton_read(storage, CONTRACT_LIST_KEY) +} + +// Admin commands + +pub fn admin_commands_r(storage: &S) -> ReadonlyBucket { + bucket_read(ADMIN_COMMANDS_KEY, storage) +} + +pub fn admin_commands_w(storage: &mut S) -> Bucket { + bucket(ADMIN_COMMANDS_KEY, storage) +} + +pub fn admin_commands_list_w(storage: &mut S) -> Singleton> { + singleton(storage, ADMIN_COMMANDS_LIST_KEY) +} + +pub fn admin_commands_list_r(storage: &S) -> ReadonlySingleton> { + singleton_read(storage, ADMIN_COMMANDS_LIST_KEY) +} diff --git a/contracts/governance/src/test.rs b/contracts/governance/src/test.rs new file mode 100644 index 000000000..a7eae36bb --- /dev/null +++ b/contracts/governance/src/test.rs @@ -0,0 +1,155 @@ +#[cfg(test)] +mod tests { + use crate::contract; + use cosmwasm_std::{ + coins, + from_binary, + testing::{mock_dependencies, mock_env}, + Api, + Extern, + HumanAddr, + Querier, + Storage, + Uint128, + }; + use shade_protocol::{ + asset::Contract, + generic_response::ResponseStatus, + governance, + governance::proposal::{ProposalStatus, QueriedProposal}, + }; + + #[test] + fn get_proposals_by_status() { + let mut deps = mock_dependencies(20, &coins(0, "")); + + // Initialize governance contract. + let env = mock_env("creator", &coins(0, "")); + let governance_init_msg = governance::InitMsg { + admin: None, + // The next governance votes will not require voting + staker: None, + funding_token: Contract { + address: HumanAddr::from(""), + code_hash: String::from(""), + }, + funding_amount: Uint128(1000000), + funding_deadline: 180, + voting_deadline: 180, + // 5 shade is the minimum + quorum: Uint128(5000000), + }; + let res = contract::init(&mut deps, env, governance_init_msg).unwrap(); + assert_eq!(1, res.messages.len()); + + // Initialized governance contract has no proposals. + let res = contract::query(&deps, governance::QueryMsg::GetProposals { + start: Uint128(0), + end: Uint128(100), + status: Some(ProposalStatus::Funding), + }) + .unwrap(); + let value: governance::QueryAnswer = from_binary(&res).unwrap(); + match value { + governance::QueryAnswer::Proposals { proposals } => { + assert_eq!(0, proposals.len()); + } + _ => { + panic!("Received wrong answer") + } + } + + // Create a proposal on governance contract. + let env = mock_env("creator", &coins(0, "")); + let res = contract::handle(&mut deps, env, governance::HandleMsg::CreateProposal { + target_contract: String::from(governance::GOVERNANCE_SELF), + proposal: serde_json::to_string(&governance::HandleMsg::AddAdminCommand { + name: "random data here".to_string(), + proposal: "{\"update_config\":{\"unbond_time\": {}, \"admin\": null}}".to_string(), + }) + .unwrap(), + description: String::from("Proposal on governance contract"), + }) + .unwrap(); + let value: governance::HandleAnswer = from_binary(&res.data.unwrap()).unwrap(); + match value { + governance::HandleAnswer::CreateProposal { + status, + proposal_id, + } => { + assert_eq!(ResponseStatus::Success, status); + assert!(!proposal_id.is_zero()); + } + _ => { + panic!("Received wrong answer") + } + } + + // Now we should have single proposal in `funding`. + + // Should return this proposal when no specific status is specified. + assert_get_proposals( + &deps, + governance::QueryMsg::GetProposals { + start: Uint128(0), + end: Uint128(100), + status: None, + }, + |proposals| { + assert_eq!(1, proposals.len()); + assert_eq!(proposals[0].status, ProposalStatus::Funding); + }, + ); + + // Should return this proposal when `funding` status is specified. + assert_get_proposals( + &deps, + governance::QueryMsg::GetProposals { + start: Uint128(0), + end: Uint128(100), + status: Some(ProposalStatus::Funding), + }, + |proposals| { + assert_eq!(1, proposals.len()); + assert_eq!(proposals[0].status, ProposalStatus::Funding); + }, + ); + + // Shouldn't return this proposal when querying by status different from `funding`. + assert_get_proposals( + &deps, + governance::QueryMsg::GetProposals { + start: Uint128(0), + end: Uint128(100), + status: Some(ProposalStatus::Voting), + }, + |proposals| { + assert_eq!(0, proposals.len()); + }, + ); + } + + /// + /// Assert via assertFn on the result of governance::QueryMsg::GetProposals contract call. + /// + /// # Arguments + /// + /// * 'deps' - External contract dependencies + /// * 'msg' - The message data + /// * 'assert_fn' - A bunch of assert statements to be performed on contract call response + /// + pub fn assert_get_proposals( + deps: &Extern, + msg: governance::QueryMsg, + assert_fn: fn(result: Vec), + ) { + let res = contract::query(&deps, msg).unwrap(); + let value: governance::QueryAnswer = from_binary(&res).unwrap(); + match value { + governance::QueryAnswer::Proposals { proposals } => assert_fn(proposals), + _ => { + panic!("Received wrong answer") + } + } + } +} diff --git a/contracts/initializer/Cargo.toml b/contracts/initializer/Cargo.toml index debd3dfda..1fbda855d 100644 --- a/contracts/initializer/Cargo.toml +++ b/contracts/initializer/Cargo.toml @@ -15,17 +15,6 @@ exclude = [ [lib] crate-type = ["cdylib", "rlib"] -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] default = [] # for quicker tests, cargo test --lib @@ -34,10 +23,10 @@ backtraces = ["cosmwasm-std/backtraces"] debug-print = ["cosmwasm-std/debug-print"] [dependencies] -cosmwasm-schema = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-std = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-storage = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -secret-toolkit = { git = "https://github.com/enigmampc/secret-toolkit", branch = "debug-print"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/initializer/README.md b/contracts/initializer/README.md index 471ee5d0a..bca9c0cd6 100644 --- a/contracts/initializer/README.md +++ b/contracts/initializer/README.md @@ -1,66 +1,50 @@ -# Secret Contracts Starter Pack -This is a template to build secret contracts in Rust to run in -[Secret Network](https://github.com/enigmampc/SecretNetwork). -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. - -## Creating a new repo from template - -Assuming you have a recent version of rust and cargo installed (via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: - -First, install -[cargo-generate](https://github.com/ashleygwilliams/cargo-generate). -Unless you did that before, run this line now: - -```sh -cargo install cargo-generate --features vendored-openssl -``` - -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: - -```sh -cargo generate --git https://github.com/enigmampc/secret-template.git --name YOUR_NAME_HERE -``` - -You will now have a new folder called `YOUR_NAME_HERE` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git checkout -b master # in case you generate from non-master -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin master +# Mint Contract +* [Introduction](#Introduction) +* [Sections](#Sections) + * [Init](#Init) + * [User](#User) + * Queries + * [GetContracts](#GetContracts) + +# Introduction +Contract responsible to initialize the snip20s and keeping the their initial states public + +# Sections + +## Init +##### Request +|Name |Type |Description | optional | +|-----------------|--------------------|------------------------------|----------| +|snip20_id | u64 | The uploaded contract's ID | no | +|snip20_code_hash | String | The uploaded contract's hash | no | +|shade | Snip20ContractInfo | Initial state for the Snip20 | no | +|silk | Snip20ContractInfo | Initial state for the Snip20 | no | + +##User + +### Queries + +#### GetContracts +Gets the contract's initialized snip20s and their initial balances +#### Response +```json +{ + "contracts": { + "contracts": ["Init History"] + } +} ``` -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://www.cosmwasm.com/docs/getting-started/intro) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -You can also find lots of useful recipes in the `Makefile` which you can use -if you have `make` installed (very recommended. at least check them out). - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. +## Snip20ContractInfo +Type used to init the snip20s +```json +{ + "snip20_contract_info": { + "label": "Initialized label", + "admin": "Optional admin", + "prng_seed": "Randomizer seed", + "initial_balances": "Initial snip20 balances" + } +} +``` \ No newline at end of file diff --git a/contracts/initializer/rustfmt.toml b/contracts/initializer/rustfmt.toml deleted file mode 100644 index 11a85e6a9..000000000 --- a/contracts/initializer/rustfmt.toml +++ /dev/null @@ -1,15 +0,0 @@ -# stable -newline_style = "unix" -hard_tabs = false -tab_spaces = 4 - -# unstable... should we require `rustup run nightly cargo fmt` ? -# or just update the style guide when they are stable? -#fn_single_line = true -#format_code_in_doc_comments = true -#overflow_delimited_expr = true -#reorder_impl_items = true -#struct_field_align_threshold = 20 -#struct_lit_single_line = true -#report_todo = "Always" - diff --git a/contracts/initializer/src/contract.rs b/contracts/initializer/src/contract.rs index df1f1b309..1d2aa7e81 100644 --- a/contracts/initializer/src/contract.rs +++ b/contracts/initializer/src/contract.rs @@ -1,17 +1,32 @@ -use cosmwasm_std::{debug_print, to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, StdError, StdResult, Storage, HumanAddr}; -use crate::state::{config_w}; -use shade_protocol::initializer::{InitMsg, InitializerConfig, HandleMsg, QueryMsg, Snip20InitHistory}; +use crate::{query::query_contracts, state::config_w}; +use cosmwasm_std::{ + debug_print, + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdResult, + Storage, +}; use secret_toolkit::utils::InitCallback; -use crate::query::query_contracts; +use shade_protocol::initializer::{ + HandleMsg, + InitMsg, + InitializerConfig, + QueryMsg, + Snip20InitHistory, +}; pub fn init( deps: &mut Extern, env: Env, msg: InitMsg, ) -> StdResult { - let mut state = InitializerConfig { - contracts: vec![], - }; + let mut state = InitializerConfig { contracts: vec![] }; let mut messages = vec![]; @@ -21,7 +36,7 @@ pub fn init( enable_deposit: Option::from(false), enable_redeem: Option::from(false), enable_mint: Option::from(true), - enable_burn: Option::from(true) + enable_burn: Option::from(true), }); // Initialize Silk @@ -29,63 +44,66 @@ pub fn init( name: "Silk".to_string(), admin: Option::from(match msg.silk.admin { None => env.message.sender.clone(), - Some(admin) => admin + Some(admin) => admin, }), symbol: "SILK".to_string(), decimals: 6, initial_balances: msg.silk.initial_balances.clone(), prng_seed: msg.silk.prng_seed, - config: coin_config.clone() + config: coin_config.clone(), }; state.contracts.push(Snip20InitHistory { label: msg.silk.label.clone(), balances: msg.silk.initial_balances.clone(), }); - messages.push(silk_init_msg.to_cosmos_msg(msg.silk.label.clone(), - msg.snip20_id, - msg.snip20_code_hash.clone(), - None)?); - + messages.push(silk_init_msg.to_cosmos_msg( + msg.silk.label.clone(), + msg.snip20_id, + msg.snip20_code_hash.clone(), + None, + )?); // Initialize Shade let shade_init_msg = shade_protocol::snip20::InitMsg { name: "Shade".to_string(), admin: Option::from(match msg.shade.admin { None => env.message.sender.clone(), - Some(admin) => admin + Some(admin) => admin, }), symbol: "SHD".to_string(), decimals: 6, initial_balances: msg.shade.initial_balances.clone(), prng_seed: msg.shade.prng_seed, - config: coin_config.clone() + config: coin_config, }; - state.contracts.push(Snip20InitHistory{ + state.contracts.push(Snip20InitHistory { label: msg.shade.label.clone(), - balances: msg.shade.initial_balances.clone() + balances: msg.shade.initial_balances.clone(), }); - messages.push(shade_init_msg.to_cosmos_msg(msg.shade.label.clone(), - msg.snip20_id, - msg.snip20_code_hash.clone(), - None)?); + messages.push(shade_init_msg.to_cosmos_msg( + msg.shade.label.clone(), + msg.snip20_id, + msg.snip20_code_hash.clone(), + None, + )?); debug_print!("Contract was initialized by {}", env.message.sender); config_w(&mut deps.storage).save(&state)?; Ok(InitResponse { messages, - log: vec![] + log: vec![], }) } pub fn handle( - deps: &mut Extern, - env: Env, - msg: HandleMsg, + _deps: &mut Extern, + _env: Env, + _msg: HandleMsg, ) -> StdResult { - return Ok(HandleResponse { + Ok(HandleResponse { messages: vec![], log: vec![], - data: None + data: None, }) } @@ -94,6 +112,6 @@ pub fn query( msg: QueryMsg, ) -> StdResult { match msg { - QueryMsg::Contracts {} => to_binary(&query_contracts(deps)?), + QueryMsg::GetContracts {} => to_binary(&query_contracts(deps)?), } -} \ No newline at end of file +} diff --git a/contracts/initializer/src/lib.rs b/contracts/initializer/src/lib.rs index eefc8eff7..03ea1d570 100644 --- a/contracts/initializer/src/lib.rs +++ b/contracts/initializer/src/lib.rs @@ -1,12 +1,17 @@ pub mod contract; -pub mod state; pub mod query; +pub mod state; #[cfg(target_arch = "wasm32")] mod wasm { use super::contract; use cosmwasm_std::{ - do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, }; #[no_mangle] diff --git a/contracts/initializer/src/query.rs b/contracts/initializer/src/query.rs index 8cd1b6731..4d01c5b68 100644 --- a/contracts/initializer/src/query.rs +++ b/contracts/initializer/src/query.rs @@ -1,7 +1,11 @@ -use cosmwasm_std::{Storage, Api, Querier, Extern, StdResult, to_binary, Binary}; -use shade_protocol::initializer::{QueryMsg, ContractsAnswer}; use crate::state::config_r; +use cosmwasm_std::{Api, Extern, Querier, StdResult, Storage}; +use shade_protocol::initializer::QueryAnswer; -pub fn query_contracts(deps: &Extern) -> StdResult { - Ok(ContractsAnswer { contracts: config_r(&deps.storage).load()?.contracts }) -} \ No newline at end of file +pub fn query_contracts( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::Contracts { + contracts: config_r(&deps.storage).load()?.contracts, + }) +} diff --git a/contracts/initializer/src/state.rs b/contracts/initializer/src/state.rs index a29c0bd26..7f802ec07 100644 --- a/contracts/initializer/src/state.rs +++ b/contracts/initializer/src/state.rs @@ -1,7 +1,4 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use cosmwasm_std::{CanonicalAddr, Storage}; +use cosmwasm_std::Storage; use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; use shade_protocol::initializer::InitializerConfig; diff --git a/contracts/micro_mint/Cargo.toml b/contracts/micro_mint/Cargo.toml index a13490164..2c8d43c3d 100644 --- a/contracts/micro_mint/Cargo.toml +++ b/contracts/micro_mint/Cargo.toml @@ -2,6 +2,7 @@ name = "micro_mint" version = "0.1.0" authors = [ + "Guy Garcia ", "Jackson Swenson ", ] edition = "2018" @@ -17,17 +18,6 @@ exclude = [ [lib] crate-type = ["cdylib", "rlib"] -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] default = [] # for quicker tests, cargo test --lib @@ -36,10 +26,10 @@ backtraces = ["cosmwasm-std/backtraces"] debug-print = ["cosmwasm-std/debug-print"] [dependencies] -cosmwasm-schema = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-std = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-storage = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -secret-toolkit = { git = "https://github.com/enigmampc/secret-toolkit", branch = "debug-print"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/micro_mint/README.md b/contracts/micro_mint/README.md index 447a28a66..9dd33cbf9 100644 --- a/contracts/micro_mint/README.md +++ b/contracts/micro_mint/README.md @@ -1,20 +1,23 @@ + # Mint Contract * [Introduction](#Introduction) * [Sections](#Sections) * [Init](#Init) * [Admin](#Admin) * Messages - * [Migrate](#Migrate) * [UpdateConfig](#UpdateConfig) + * [UpdateMintLimit](#UpdateMintLimit) * [RegisterAsset](#RegisterAsset) - * Queries - * [GetNativeAsset](#GetNativeAsset) - * [GetConfig](#GetConfig) - * [SupportedAssets](#SupportedAssets) - * [GetAsset](#getAsset) + * [RemoveAsset](#RemoveAsset) * [User](#User) * Messages * [Receive](#Receive) + * Queries + * [GetNativeAsset](#GetNativeAsset) + * [GetConfig](#GetConfig) + * [GetMintLimit](#GetMintLimit) + * [GetSupportedAssets](#GetSupportedAssets) + * [GetAsset](#GetAsset) # Introduction Contract responsible to mint a paired snip20 asset @@ -22,25 +25,29 @@ Contract responsible to mint a paired snip20 asset ## Init ##### Request -|Name |Type |Description | optional | -|-------------|----------|-------------------------------------------------------------------------------------------------------------------|----------| -|admin | string | New contract owner; SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | yes | -|native_asset | Contract | Asset to mint | no | -|peg | String | Symbol to peg to when querying oracle (defaults to native_asset symbol) | yes | -|treasury | Contract | Treasury contract | yes | -|oracle | Contract | Oracle contract | no | - +|Name |Type |Description | optional | +|-----------------|------------|-------------------------------------------------------------------------------|----------| +|admin | string | New contract owner; SHOULD be a valid bech32 address | yes | +|native_asset | Contract | Asset to mint | no | +|oracle | Contract | Oracle contract | no | +|peg | String | Symbol to peg to when querying oracle (defaults to native_asset symbol) | yes | +|treasury | Contract | Treasury contract | yes | +|secondary_burn | HumanAddrr | Where non-burnable assets will go | yes | +|start_epoch | String | The starting epoch | yes | +|epoch_frequency | String | The frequency in which the mint limit resets, if 0 then no limit is enforced | yes | +|epoch_mint_limit | String | The limit of uTokens to mint per epoch | yes | ## Admin ### Messages -### Migrate -Migrates all the contracts state and data into a new contract -#### Request -| Name | Type | Description | optional | -|----------| -------|---------------------|----------| -|label | String | Contract label name | no | -|code_id | u64 | Contract ID | no | -|code_hash | String | Contract code hash | no | +#### UpdateConfig +Updates the given values +##### Request +|Name |Type |Description | optional | +|---------------|------------|-------------------------------------------------------|----------| +|admin | string | New contract admin; SHOULD be a valid bech32 address | yes | +|oracle | Contract | Oracle contract | yes | +|treasury | Contract | Treasury contract | yes | +|secondary_burn | HumanAddrr | Where non-burnable assets will go | yes | ##### Response ```json { @@ -50,18 +57,18 @@ Migrates all the contracts state and data into a new contract } ``` -#### UpdateConfig -Updates the given values +#### UpdateMintLimit +Updates the mint limit and epoch time ##### Request -|Name |Type |Description | optional | -|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| -|owner | string | New contract owner; SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | yes | -|treasury | Contract | Treasury contract | yes | -|oracle | Contract | Oracle contract | yes | +|Name |Type |Description | optional | +|-----------------|----------|--------------------------------------------------------------------------------|----------| +|start_epoch | String | The starting epoch | yes | +|epoch_frequency | String | The frequency in which the mint limit resets, if 0 then no limit is enforced | yes | +|epoch_mint_limit | String | The limit of uTokens to mint per epoch | yes | ##### Response ```json { - "update_config": { + "update_mint_limit": { "status": "success" } } @@ -70,11 +77,10 @@ Updates the given values #### RegisterAsset Registers a supported asset. The asset must be SNIP-20 compliant since [RegisterReceive](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md#RegisterReceive) is called. -Note: Will return an error if there's an asset with that address already registered. ##### Request -|Name |Type |Description | optional | -|------------|--------|-----------------------------------------------------------------------------------------------------------------------|----------| -|contract | Contract | Type explained [here](#Contract) | no | +|Name |Type |Description | optional | +|------------|--------|-------------------------------------|----------| +|contract | Contract | Type explained [here](#Contract) | no | ##### Response ```json { @@ -84,6 +90,33 @@ Note: Will return an error if there's an asset with that address already registe } ``` +#### RemoveAsset +Remove a registered asset. +##### Request +|Name |Type |Description | optional | +|------------|--------|--------------------------------|----------| +|address | String | The asset to remove's address | no | +##### Response +```json +{ + "remove_asset": { + "status": "success" + } +} +``` + +##User + +### Messages + +#### Receive +To mint the user must use a supported asset's send function and send the amount over to the contract's address. The contract will take care of the rest. + +In the msg field of a snip20 send command you must send a base64 encoded json like this one +```json +{"minimum_expected_amount": "Uint128" } +``` + ### Queries #### GetNativeAsset @@ -105,7 +138,7 @@ Gets the contract's configuration variables { "config": { "config": { - "owner": "Owner address", + "admin": "Owner address", "oracle": { "address": "Asset contract address", "code_hash": "Asset callback code hash" @@ -114,13 +147,30 @@ Gets the contract's configuration variables "address": "Asset contract address", "code_hash": "Asset callback code hash" }, + "secondary_burn": "Optional burn address", "activated": "Boolean of contract's actviation status" } } } ``` -#### SupportedAssets +#### GetMintLimit +Gets the contract's configuration variables +##### Response +```json +{ + "limit": { + "mint_limit": { + "frequency": "Frequency per epoch reset", + "mint_capacity": "Mint capacity per epoch", + "total_minted": "Total minted in current epoch", + "next_epoch": "Timestamp for the next epoch" + } + } +} +``` + +#### GetSupportedAssets Get all the contract's supported assets. ##### Response ```json @@ -153,20 +203,6 @@ Get specific information on a supported asset. } ``` -##User - -### Messages - -#### Receive -To mint the user must use a supported asset's send function and send the amount over to the contract's address. The contract will take care of the rest. - -In the msg field of a snip20 send command you must send a base64 encoded json like this one -```json -{"minimum_expected_amount": "Uint128", "mint_type": { "coin_to_silk": { } } } -``` - -The currently supported mint types are ```coin_to_silk``` , ```coin_to_shade```, ```convert_to_silk``` and ```convert_to_shade``` - ## Contract Type used in many of the admin commands ```json diff --git a/contracts/micro_mint/rustfmt.toml b/contracts/micro_mint/rustfmt.toml deleted file mode 100644 index 11a85e6a9..000000000 --- a/contracts/micro_mint/rustfmt.toml +++ /dev/null @@ -1,15 +0,0 @@ -# stable -newline_style = "unix" -hard_tabs = false -tab_spaces = 4 - -# unstable... should we require `rustup run nightly cargo fmt` ? -# or just update the style guide when they are stable? -#fn_single_line = true -#format_code_in_doc_comments = true -#overflow_delimited_expr = true -#reorder_impl_items = true -#struct_field_align_threshold = 20 -#struct_lit_single_line = true -#report_todo = "Always" - diff --git a/contracts/micro_mint/src/contract.rs b/contracts/micro_mint/src/contract.rs index e7ed64852..1dc2cb0e9 100644 --- a/contracts/micro_mint/src/contract.rs +++ b/contracts/micro_mint/src/contract.rs @@ -1,59 +1,83 @@ use cosmwasm_std::{ - debug_print, to_binary, Api, Binary, - Env, Extern, HandleResponse, InitResponse, - Querier, StdResult, Storage, + debug_print, + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdResult, + Storage, + Uint128, }; use secret_toolkit::snip20::token_info_query; use shade_protocol::{ - micro_mint::{ - InitMsg, HandleMsg, - QueryMsg, Config, - }, - snip20::{ - Snip20Asset, - token_config_query, - }, + micro_mint::{Config, HandleMsg, InitMsg, QueryMsg}, + snip20::{token_config_query, Snip20Asset}, }; use crate::{ - state::{ - config_w, - native_asset_w, - asset_peg_w, - asset_list_w, - }, - handle, query, + handle, + query, + state::{asset_list_w, asset_peg_w, config_w, limit_w, native_asset_w}, }; +use shade_protocol::micro_mint::MintLimit; pub fn init( deps: &mut Extern, env: Env, msg: InitMsg, ) -> StdResult { - let state = Config { - owner: match msg.admin { - None => { env.message.sender.clone() } - Some(admin) => { admin } + admin: match msg.admin { + None => env.message.sender.clone(), + Some(admin) => admin, }, oracle: msg.oracle, treasury: msg.treasury, + secondary_burn: msg.secondary_burn, activated: true, }; + // Set the minting limit + let mut limit = MintLimit { + frequency: match msg.epoch_frequency { + None => 0, + Some(frequency) => frequency.u128() as u64, + }, + mint_capacity: match msg.epoch_mint_limit { + None => Uint128(0), + Some(capacity) => capacity, + }, + total_minted: Uint128(0), + next_epoch: match msg.epoch_frequency { + None => 0, + Some(frequency) => env.block.time + frequency.u128() as u64, + }, + }; + // Override the next epoch + if let Some(next_epoch) = msg.start_epoch { + limit.next_epoch = next_epoch.u128() as u64; + } + + limit_w(&mut deps.storage).save(&limit)?; + config_w(&mut deps.storage).save(&state)?; let token_info = token_info_query( - &deps.querier, 1, - msg.native_asset.code_hash.clone(), - msg.native_asset.address.clone())?; + &deps.querier, + 1, + msg.native_asset.code_hash.clone(), + msg.native_asset.address.clone(), + )?; - let token_config = token_config_query(&deps.querier, - msg.native_asset.clone())?; + let token_config = token_config_query(&deps.querier, msg.native_asset.clone())?; let peg = match msg.peg { - Some(p) => { p } - None => { token_info.symbol.clone() } + Some(p) => p, + None => token_info.symbol.clone(), }; asset_peg_w(&mut deps.storage).save(&peg)?; @@ -71,7 +95,7 @@ pub fn init( Ok(InitResponse { messages: vec![], - log: vec![] + log: vec![], }) } @@ -82,20 +106,27 @@ pub fn handle( ) -> StdResult { match msg { HandleMsg::UpdateConfig { - owner, + admin: owner, oracle, treasury, - } => handle::try_update_config(deps, env, owner, oracle, treasury), - HandleMsg::RegisterAsset { - contract, - commission, - } => handle::try_register_asset(deps, &env, &contract, commission), + secondary_burn, + } => handle::try_update_config(deps, env, owner, oracle, treasury, secondary_burn), + HandleMsg::UpdateMintLimit { + start_epoch, + epoch_frequency, + epoch_limit, + } => handle::try_update_limit(deps, env, start_epoch, epoch_frequency, epoch_limit), + HandleMsg::RegisterAsset { contract, capture } => { + handle::try_register_asset(deps, &env, &contract, capture) + } + HandleMsg::RemoveAsset { address } => handle::try_remove_asset(deps, &env, address), HandleMsg::Receive { sender, from, amount, msg, - ..} => handle::try_burn(deps, env, sender, from, amount, msg), + .. + } => handle::try_burn(deps, env, sender, from, amount, msg), } } @@ -108,5 +139,6 @@ pub fn query( QueryMsg::GetSupportedAssets {} => to_binary(&query::supported_assets(deps)?), QueryMsg::GetAsset { contract } => to_binary(&query::asset(deps, contract)?), QueryMsg::GetConfig {} => to_binary(&query::config(deps)?), + QueryMsg::GetMintLimit {} => to_binary(&query::limit(deps)?), } } diff --git a/contracts/micro_mint/src/handle.rs b/contracts/micro_mint/src/handle.rs index 9fec32750..bfc57476d 100644 --- a/contracts/micro_mint/src/handle.rs +++ b/contracts/micro_mint/src/handle.rs @@ -1,35 +1,44 @@ -use std::convert::TryFrom; -use cosmwasm_std::{debug_print, to_binary, Api, Binary, Env, Extern, HandleResponse, Querier, StdError, StdResult, Storage, CosmosMsg, HumanAddr, Uint128, from_binary, Empty}; +use cosmwasm_std::{ + debug_print, + from_binary, + to_binary, + Api, + Binary, + CosmosMsg, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, + Uint128, +}; use secret_toolkit::{ - snip20::{ - token_info_query, - mint_msg, burn_msg, send_msg, - register_receive_msg, - }, + snip20::{burn_msg, mint_msg, register_receive_msg, send_msg, token_info_query}, + utils::Query, }; -use secret_toolkit::utils::Query; use shade_protocol::{ - micro_mint::{ - HandleAnswer, - Config, - SupportedAsset, - }, - mint::SnipMsgHook, - snip20::{Snip20Asset, token_config_query, TokenConfig}, - oracle::{ - QueryMsg::GetPrice, - }, - band::ReferenceData, asset::Contract, + band::ReferenceData, generic_response::ResponseStatus, + micro_mint::{Config, HandleAnswer, SupportedAsset}, + mint::MintMsgHook, + oracle::QueryMsg::Price, + snip20::{token_config_query, Snip20Asset, TokenConfig}, }; +use std::{cmp::Ordering, convert::TryFrom}; use crate::state::{ - config_w, config_r, - native_asset_r, - asset_peg_r, - assets_w, assets_r, asset_list_w, + asset_peg_r, + assets_r, + assets_w, + config_r, + config_w, + limit_w, + native_asset_r, total_burned_w, }; @@ -41,112 +50,208 @@ pub fn try_burn( amount: Uint128, msg: Option, ) -> StdResult { - let config = config_r(&deps.storage).load()?; // Check if contract enabled if !config.activated { return Err(StdError::Unauthorized { backtrace: None }); } + let mint_asset = native_asset_r(&deps.storage).load()?; + // Prevent sender to be native asset - if native_asset_r(&deps.storage).load()?.contract.address == env.message.sender { - return Err(StdError::generic_err("Sender cannot be the same as the native asset.")) + if mint_asset.contract.address == env.message.sender { + return Err(StdError::generic_err( + "Sender cannot be the same as the native asset.", + )); } // Check that sender is a supported snip20 asset let assets = assets_r(&deps.storage); let burn_asset = match assets.may_load(env.message.sender.to_string().as_bytes())? { Some(supported_asset) => { - debug_print!("Found Burn Asset: {} {}", - &supported_asset.asset.token_info.symbol, - env.message.sender.to_string()); + debug_print!( + "Found Burn Asset: {} {}", + &supported_asset.asset.token_info.symbol, + env.message.sender.to_string() + ); supported_asset - }, - None => return Err(StdError::NotFound { - kind: env.message.sender.to_string(), - backtrace: None - }), + } + None => { + return Err(StdError::NotFound { + kind: env.message.sender.to_string(), + backtrace: None, + }); + } }; - // Setup msgs + // This will calculate the total mint value + let amount_to_mint: Uint128 = mint_amount(deps, amount, &burn_asset, &mint_asset)?; + let mut messages = vec![]; - let msgs: SnipMsgHook = match msg { + let msgs: MintMsgHook = match msg { Some(x) => from_binary(&x)?, None => return Err(StdError::generic_err("data cannot be empty")), }; + // Check against slippage amount + if amount_to_mint < msgs.minimum_expected_amount { + return Err(StdError::generic_err( + "Mint amount is less than the minimum expected.", + )); + } + + // Check against mint cap + let mut limit_storage = limit_w(&mut deps.storage); + let mut limit = limit_storage.load()?; + + // When frequency is 0 it means that mint limits are disabled + if limit.frequency != 0 { + // Reset total and next epoch + if limit.next_epoch <= env.block.time { + limit.next_epoch = env.block.time + limit.frequency; + limit.total_minted = Uint128(0); + } + + let new_total = limit.total_minted + amount_to_mint; + + if new_total > limit.mint_capacity { + return Err(StdError::generic_err( + "Amount to be minted exceeds mint capacity", + )); + } + + limit.total_minted = new_total; + + limit_storage.save(&limit)?; + } + let mut burn_amount = amount; if let Some(treasury) = config.treasury { - // Ignore commission if the set commission is 0 - if burn_asset.commission != Uint128(0) { - let commission_amount = calculate_commission(amount, burn_asset.commission); + // Ignore capture if the set capture is 0 + if burn_asset.capture != Uint128(0) { + let capture_amount = calculate_capture(amount, burn_asset.capture); // Commission to treasury - messages.push(send_msg(treasury.address, - commission_amount, - None, - None, - 1, - burn_asset.asset.contract.code_hash.clone(), - burn_asset.asset.contract.address.clone())?); - - burn_amount = (amount - commission_amount)?; + messages.push(send_msg( + treasury.address, + capture_amount, + None, + None, + None, + 1, + burn_asset.asset.contract.code_hash.clone(), + burn_asset.asset.contract.address.clone(), + )?); + + burn_amount = (amount - capture_amount)?; } } - //TODO: if token_config is None, or cant burn, need to trash - // Try to burn - match burn_asset.asset.token_config { - Some(ref conf) => { - if conf.burn_enabled { - messages.push(burn_msg(burn_amount, - None, - 256, - burn_asset.asset.contract.code_hash.clone(), - burn_asset.asset.contract.address.clone())?); - } - } - None => { + if let Some(token_config) = &burn_asset.asset.token_config { + if token_config.burn_enabled { + messages.push(burn_msg( + burn_amount, + None, + None, + 256, + burn_asset.asset.contract.code_hash.clone(), + burn_asset.asset.contract.address.clone(), + )?); + } else if let Some(recipient) = config.secondary_burn { + messages.push(send_msg( + recipient, + burn_amount, + None, + None, + None, + 1, + burn_asset.asset.contract.code_hash.clone(), + burn_asset.asset.contract.address.clone(), + )?) } + } else if let Some(recipient) = config.secondary_burn { + messages.push(send_msg( + recipient, + burn_amount, + None, + None, + None, + 1, + burn_asset.asset.contract.code_hash.clone(), + burn_asset.asset.contract.address.clone(), + )?) } // Update burned amount total_burned_w(&mut deps.storage).update( burn_asset.asset.contract.address.to_string().as_bytes(), - |burned| { - match burned { - Some(burned) => { Ok(burned + burn_amount) } - None => { Ok(burn_amount) } - } - })?; + |burned| match burned { + Some(burned) => Ok(burned + burn_amount), + None => Ok(burn_amount), + }, + )?; let mint_asset = native_asset_r(&deps.storage).load()?; // This will calculate the total mint value - // Amount minted does not consider commission - let amount_to_mint: Uint128 = mint_amount(&deps, amount, &burn_asset, &mint_asset)?; + let amount_to_mint: Uint128 = mint_amount(deps, amount, &burn_asset, &mint_asset)?; + // Check against slippage amount if amount_to_mint < msgs.minimum_expected_amount { - return Err(StdError::generic_err("Mint amount is less than the minimum expected.")) + return Err(StdError::generic_err( + "Mint amount is less than the minimum expected.", + )); + } + + // Check against mint cap + let mut limit_storage = limit_w(&mut deps.storage); + let mut limit = limit_storage.load()?; + + // When frequency is 0 it means that mint limits are disabled + if limit.frequency != 0 { + // Reset total and next epoch + if limit.next_epoch <= env.block.time { + limit.next_epoch = env.block.time + limit.frequency; + limit.total_minted = Uint128(0); + } + + let new_total = limit.total_minted + amount_to_mint; + + if new_total > limit.mint_capacity { + return Err(StdError::generic_err( + "Amount to be minted exceeds mint capacity", + )); + } + + limit.total_minted = new_total; + + limit_storage.save(&limit)?; } - debug_print!("Minting: {} {}", amount_to_mint, &mint_asset.token_info.symbol); + debug_print!( + "Minting: {} {}", + amount_to_mint, + &mint_asset.token_info.symbol + ); - messages.push(mint_msg(from, - amount_to_mint, - None, - 256, - mint_asset.contract.code_hash.clone(), - mint_asset.contract.address.clone())?); + messages.push(mint_msg( + from, + amount_to_mint, + None, + None, + 256, + mint_asset.contract.code_hash.clone(), + mint_asset.contract.address, + )?); Ok(HandleResponse { messages, log: vec![], - data: Some( to_binary( &HandleAnswer::Burn { + data: Some(to_binary(&HandleAnswer::Burn { status: ResponseStatus::Success, - mint_amount: amount_to_mint - } )? ), + mint_amount: amount_to_mint, + })?), }) } @@ -156,11 +261,11 @@ pub fn try_update_config( owner: Option, oracle: Option, treasury: Option, + secondary_burn: Option, ) -> StdResult { - let config = config_r(&deps.storage).load()?; // Check if admin - if env.message.sender != config.owner { + if env.message.sender != config.admin { return Err(StdError::Unauthorized { backtrace: None }); } // Check if contract enabled @@ -172,7 +277,7 @@ pub fn try_update_config( let mut config = config_w(&mut deps.storage); config.update(|mut state| { if let Some(owner) = owner { - state.owner = owner; + state.admin = owner; } if let Some(oracle) = oracle { state.oracle = oracle; @@ -180,14 +285,67 @@ pub fn try_update_config( if let Some(treasury) = treasury { state.treasury = Some(treasury); } + if let Some(secondary_burn) = secondary_burn { + state.secondary_burn = Some(secondary_burn) + } Ok(state) })?; Ok(HandleResponse { messages: vec![], log: vec![], - data: Some( to_binary( &HandleAnswer::UpdateConfig { - status: ResponseStatus::Success } )? ) + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_update_limit( + deps: &mut Extern, + env: Env, + start_epoch: Option, + epoch_frequency: Option, + epoch_limit: Option, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + // Check if admin + if env.message.sender != config.admin { + return Err(StdError::Unauthorized { backtrace: None }); + } + // Check if contract enabled + if !config.activated { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Reset limit and set new limits + let mut limit = limit_w(&mut deps.storage); + limit.update(|mut state| { + if let Some(frequency) = epoch_frequency { + state.frequency = frequency.u128() as u64; + } + if let Some(limit) = epoch_limit { + state.mint_capacity = limit + } + // Reset total minted + state.total_minted = Uint128(0); + + // Reset next epoch + if state.frequency == 0 { + state.next_epoch = 0; + } else if let Some(next_epoch) = start_epoch { + state.next_epoch = next_epoch.u128() as u64; + } else { + state.next_epoch = env.block.time + state.frequency; + } + Ok(state) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateMintLimit { + status: ResponseStatus::Success, + })?), }) } @@ -195,12 +353,11 @@ pub fn try_register_asset( deps: &mut Extern, env: &Env, contract: &Contract, - commission: Option + capture: Option, ) -> StdResult { - let config = config_r(&deps.storage).load()?; // Check if admin - if env.message.sender != config.owner { + if env.message.sender != config.admin { return Err(StdError::Unauthorized { backtrace: None }); } // Check if contract enabled @@ -212,30 +369,34 @@ pub fn try_register_asset( let contract_str = contract.address.to_string(); // Add the new asset - let asset_info = token_info_query(&deps.querier, 1, - contract.code_hash.clone(), - contract.address.clone())?; + let asset_info = token_info_query( + &deps.querier, + 1, + contract.code_hash.clone(), + contract.address.clone(), + )?; - let asset_config: Option = match token_config_query(&deps.querier, contract.clone()) { - Ok(c) => { Option::from(c) } - Err(_) => { None } - }; + let asset_config: Option = + match token_config_query(&deps.querier, contract.clone()) { + Ok(c) => Option::from(c), + Err(_) => None, + }; debug_print!("Registering {}", asset_info.symbol); - assets.save(&contract_str.as_bytes(), &SupportedAsset { + assets.save(contract_str.as_bytes(), &SupportedAsset { asset: Snip20Asset { contract: contract.clone(), token_info: asset_info, token_config: asset_config, }, - // If commission is not set then default to 0 - commission: match commission { + // If capture is not set then default to 0 + capture: match capture { None => Uint128(0), - Some(value) => value - } + Some(value) => value, + }, })?; - total_burned_w(&mut deps.storage).save(&contract_str.as_bytes(), &Uint128(0))?; + total_burned_w(&mut deps.storage).save(contract_str.as_bytes(), &Uint128(0))?; // Add the asset to list asset_list_w(&mut deps.storage).update(|mut state| { @@ -244,62 +405,89 @@ pub fn try_register_asset( })?; // Register contract in asset - let mut messages = vec![]; - messages.push(register_receive(env, contract)?); + let messages = vec![register_receive(env, contract)?]; Ok(HandleResponse { messages, log: vec![], - data: Some( to_binary( - &HandleAnswer::RegisterAsset { - status: ResponseStatus::Success } - )? - ) + data: Some(to_binary(&HandleAnswer::RegisterAsset { + status: ResponseStatus::Success, + })?), }) } +pub fn try_remove_asset( + deps: &mut Extern, + _env: &Env, + address: HumanAddr, +) -> StdResult { + let address_str = address.to_string(); -pub fn register_receive ( - env: &Env, - contract: &Contract, -) -> StdResult { - let cosmos_msg = register_receive_msg( + // Remove asset from the array + asset_list_w(&mut deps.storage).update(|mut state| { + state.retain(|value| *value != address_str); + Ok(state) + })?; + + // Remove supported asset + assets_w(&mut deps.storage).remove(address_str.as_bytes()); + + // We wont remove the total burned since we want to keep track of all the burned assets + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::RemoveAsset { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn register_receive(env: &Env, contract: &Contract) -> StdResult { + register_receive_msg( env.contract_code_hash.clone(), None, 256, contract.code_hash.clone(), contract.address.clone(), - ); - - cosmos_msg + ) } pub fn mint_amount( deps: &Extern, burn_amount: Uint128, burn_asset: &SupportedAsset, - mint_asset: &Snip20Asset, + mint_asset: &Snip20Asset, ) -> StdResult { + debug_print!( + "Burning {} {} for {}", + burn_amount, + burn_asset.asset.token_info.symbol, + mint_asset.token_info.symbol + ); - - debug_print!("Burning {} {} for {}", - burn_amount, - burn_asset.asset.token_info.symbol, - mint_asset.token_info.symbol); - - let burn_price = oracle(&deps, &burn_asset.asset.token_info.symbol)?; + let burn_price = oracle(deps, burn_asset.asset.token_info.symbol.clone())?; debug_print!("Burn Price: {}", burn_price); - let mint_price = oracle(&deps, &asset_peg_r(&deps.storage).load()?)?; + let mint_price = oracle(deps, asset_peg_r(&deps.storage).load()?)?; debug_print!("Mint Price: {}", mint_price); - Ok(calculate_mint(burn_price, burn_amount, burn_asset.asset.token_info.decimals, - mint_price, mint_asset.token_info.decimals)) + Ok(calculate_mint( + burn_price, + burn_amount, + burn_asset.asset.token_info.decimals, + mint_price, + mint_asset.token_info.decimals, + )) } -pub fn calculate_mint(burn_price: Uint128, burn_amount: Uint128, burn_decimals: u8, - mint_price: Uint128, mint_decimals: u8 - ) -> Uint128 { +pub fn calculate_mint( + burn_price: Uint128, + burn_amount: Uint128, + burn_decimals: u8, + mint_price: Uint128, + mint_decimals: u8, +) -> Uint128 { // Math must only be made in integers // in_decimals = x // target_decimals = y @@ -320,39 +508,36 @@ pub fn calculate_mint(burn_price: Uint128, burn_amount: Uint128, burn_decimals: let difference: i32 = mint_decimals as i32 - burn_decimals as i32; // To avoid a mess of different types doing math - if difference < 0 { - burn_value.multiply_ratio(1u128, 10u128.pow(u32::try_from(difference.abs()).unwrap())) - } - else if difference > 0 { - Uint128(burn_value.u128() * 10u128.pow(u32::try_from(difference).unwrap())) - } - else { - burn_value + match difference.cmp(&0) { + Ordering::Greater => { + Uint128(burn_value.u128() * 10u128.pow(u32::try_from(difference).unwrap())) + } + Ordering::Less => { + burn_value.multiply_ratio(1u128, 10u128.pow(u32::try_from(difference.abs()).unwrap())) + } + Ordering::Equal => burn_value, } } -pub fn calculate_commission( - amount: Uint128, commission: Uint128 -) -> Uint128 { +pub fn calculate_capture(amount: Uint128, capture: Uint128) -> Uint128 { /* amount: total amount sent to burn (uSSCRT/uSILK/uSHD) - * commission: commission_percent * 10,000 e.g. 532 = 5.32% = .0532 + * capture: capture_percent * 10,000 e.g. 532 = 5.32% = .0532 * - * commission_amount = amount * commission / 10000 + * capture_amount = amount * capture / 10000 */ - return amount.multiply_ratio(commission, 10000u128); + amount.multiply_ratio(capture, 10000u128) } fn oracle( deps: &Extern, - symbol: &String, + symbol: String, ) -> StdResult { - let config: Config = config_r(&deps.storage).load()?; - let answer: ReferenceData = GetPrice { - symbol: symbol.to_string() - }.query(&deps.querier, - config.oracle.code_hash, - config.oracle.address)?; + let answer: ReferenceData = Price { symbol }.query( + &deps.querier, + config.oracle.code_hash, + config.oracle.address, + )?; Ok(answer.rate) } diff --git a/contracts/micro_mint/src/lib.rs b/contracts/micro_mint/src/lib.rs index 14d4d23b1..84be1cef6 100644 --- a/contracts/micro_mint/src/lib.rs +++ b/contracts/micro_mint/src/lib.rs @@ -1,7 +1,7 @@ pub mod contract; -pub mod state; pub mod handle; pub mod query; +pub mod state; #[cfg(test)] mod test; @@ -10,7 +10,12 @@ mod test; mod wasm { use super::contract; use cosmwasm_std::{ - do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, }; #[no_mangle] diff --git a/contracts/micro_mint/src/query.rs b/contracts/micro_mint/src/query.rs index 538c6d2dd..73c382760 100644 --- a/contracts/micro_mint/src/query.rs +++ b/contracts/micro_mint/src/query.rs @@ -1,43 +1,58 @@ -use cosmwasm_std::{ - Api, Extern, Querier, StdError, StdResult, Storage, -}; use crate::state::{ - config_r, - native_asset_r, + asset_list_r, asset_peg_r, assets_r, - asset_list_r, + config_r, + limit_r, + native_asset_r, total_burned_r, }; -use shade_protocol::{ - micro_mint::{ - QueryAnswer - }, -}; +use cosmwasm_std::{Api, Extern, Querier, StdError, StdResult, Storage}; +use shade_protocol::micro_mint::QueryAnswer; -pub fn native_asset(deps: &Extern) -> StdResult { - Ok(QueryAnswer::NativeAsset { asset: native_asset_r(&deps.storage).load()?, - peg: asset_peg_r(&deps.storage).load()? }) +pub fn native_asset( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::NativeAsset { + asset: native_asset_r(&deps.storage).load()?, + peg: asset_peg_r(&deps.storage).load()?, + }) } -pub fn supported_assets(deps: &Extern) -> StdResult { - Ok(QueryAnswer::SupportedAssets { assets: asset_list_r(&deps.storage).load()? }) +pub fn supported_assets( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::SupportedAssets { + assets: asset_list_r(&deps.storage).load()?, + }) } -pub fn asset(deps: &Extern, contract: String) -> StdResult { +pub fn asset( + deps: &Extern, + contract: String, +) -> StdResult { let assets = assets_r(&deps.storage); return match assets.may_load(contract.as_bytes())? { - Some(asset) => { - Ok(QueryAnswer::Asset { - asset, - burned: total_burned_r(&deps.storage).load(contract.as_bytes())?, - }) - } - None => Err(StdError::NotFound { kind: contract, backtrace: None }), + Some(asset) => Ok(QueryAnswer::Asset { + asset, + burned: total_burned_r(&deps.storage).load(contract.as_bytes())?, + }), + None => Err(StdError::NotFound { + kind: contract, + backtrace: None, + }), }; } pub fn config(deps: &Extern) -> StdResult { - Ok(QueryAnswer::Config { config: config_r(&deps.storage).load()? }) + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) +} + +pub fn limit(deps: &Extern) -> StdResult { + Ok(QueryAnswer::MintLimit { + limit: limit_r(&deps.storage).load()?, + }) } diff --git a/contracts/micro_mint/src/state.rs b/contracts/micro_mint/src/state.rs index 25084e46b..17c68cf8d 100644 --- a/contracts/micro_mint/src/state.rs +++ b/contracts/micro_mint/src/state.rs @@ -1,11 +1,21 @@ use cosmwasm_std::{Storage, Uint128}; -use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton, bucket, Bucket, bucket_read, ReadonlyBucket}; +use cosmwasm_storage::{ + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, +}; use shade_protocol::{ - micro_mint::{Config, SupportedAsset}, + micro_mint::{Config, MintLimit, SupportedAsset}, snip20::Snip20Asset, }; pub static CONFIG_KEY: &[u8] = b"config"; +pub static MINT_LIMIT: &[u8] = b"mint_limit"; pub static NATIVE_ASSET: &[u8] = b"native_asset"; pub static ASSET_PEG: &[u8] = b"asset_peg"; pub static ASSET_KEY: &[u8] = b"assets"; @@ -20,6 +30,14 @@ pub fn config_r(storage: &S) -> ReadonlySingleton { singleton_read(storage, CONFIG_KEY) } +pub fn limit_w(storage: &mut S) -> Singleton { + singleton(storage, MINT_LIMIT) +} + +pub fn limit_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, MINT_LIMIT) +} + pub fn native_asset_w(storage: &mut S) -> Singleton { singleton(storage, NATIVE_ASSET) } @@ -44,7 +62,7 @@ pub fn asset_list_r(storage: &S) -> ReadonlySingleton singleton_read(storage, ASSET_LIST_KEY) } -pub fn assets_r(storage: & S) -> ReadonlyBucket { +pub fn assets_r(storage: &S) -> ReadonlyBucket { bucket_read(ASSET_KEY, storage) } @@ -52,7 +70,7 @@ pub fn assets_w(storage: &mut S) -> Bucket { bucket(ASSET_KEY, storage) } -pub fn total_burned_r(storage: & S) -> ReadonlyBucket { +pub fn total_burned_r(storage: &S) -> ReadonlyBucket { bucket_read(BURN_COUNT_KEY, storage) } diff --git a/contracts/micro_mint/src/test.rs b/contracts/micro_mint/src/test.rs index a1f804271..6c75ae70b 100644 --- a/contracts/micro_mint/src/test.rs +++ b/contracts/micro_mint/src/test.rs @@ -1,52 +1,42 @@ #[cfg(test)] pub mod tests { use cosmwasm_std::{ - testing::{ - mock_dependencies, mock_env, MockStorage, MockApi, MockQuerier - }, - coins, from_binary, StdError, Uint128, + coins, + from_binary, + testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}, Extern, + StdError, + Uint128, }; + use mockall_double::double; use shade_protocol::{ - micro_mint::{ - QueryAnswer, InitMsg, HandleMsg, - QueryMsg, - }, asset::Contract, + micro_mint::{HandleMsg, InitMsg, QueryAnswer, QueryMsg}, }; - use mockall_double::double; use crate::{ - contract::{ - init, handle, query, - }, - handle::{ - calculate_commission, - calculate_mint, - try_burn, - }, + contract::{handle, init, query}, + handle::{calculate_capture, calculate_mint, try_burn}, }; mod mock_secret_toolkit { - - use cosmwasm_std::{Querier, HumanAddr, StdResult, Uint128}; - use secret_toolkit::snip20::TokenInfo; - - pub fn mock_token_info_query( - _querier: &Q, - _block_size: usize, - _callback_code_hash: String, - _contract_addr: HumanAddr, - ) -> StdResult { - Ok(TokenInfo { - name: "Token".to_string(), - symbol: "TKN".to_string(), - decimals: 6, - total_supply: Option::from(Uint128(150)), - }) - } - + use cosmwasm_std::{HumanAddr, Querier, StdResult, Uint128}; + use secret_toolkit::snip20::TokenInfo; + + pub fn mock_token_info_query( + _querier: &Q, + _block_size: usize, + _callback_code_hash: String, + _contract_addr: HumanAddr, + ) -> StdResult { + Ok(TokenInfo { + name: "Token".to_string(), + symbol: "TKN".to_string(), + decimals: 6, + total_supply: Option::from(Uint128(150)), + }) + } } #[double] @@ -54,13 +44,20 @@ pub mod tests { fn create_contract(address: &str, code_hash: &str) -> Contract { let env = mock_env(address.to_string(), &[]); - return Contract{ + return Contract { address: env.message.sender, - code_hash: code_hash.to_string() - } + code_hash: code_hash.to_string(), + }; } - fn dummy_init(admin: String, native_asset: Contract, oracle: Contract, peg: Option, treasury: Option, commission: Option) -> Extern { + fn dummy_init( + admin: String, + native_asset: Contract, + oracle: Contract, + peg: Option, + treasury: Option, + capture: Option, + ) -> Extern { let mut deps = mock_dependencies(20, &[]); let msg = InitMsg { admin: None, @@ -68,11 +65,15 @@ pub mod tests { oracle, peg, treasury, + secondary_burn: None, + start_epoch: None, + epoch_frequency: None, + epoch_mint_limit: None, }; let env = mock_env(admin, &coins(1000, "earth")); let _res = init(&mut deps, env, msg).unwrap(); - return deps + return deps; } #[test] @@ -86,7 +87,7 @@ pub mod tests { peg: Option::from("TKN".to_string()), treasury: Option::from(create_contract("", "")), // 1% - commission: Option::from(Uint128(100)), + capture: Option::from(Uint128(100)), }; let env = mock_env("creator", &coins(1000, "earth")); @@ -102,7 +103,7 @@ pub mod tests { let native_asset = create_contract("snip20", "hash"); let oracle = create_contract("oracle", "hash"); let treasury = create_contract("treasury", "hash"); - let commission = Uint128(100); + let capture = Uint128(100); let admin_env = mock_env("admin", &coins(1000, "earth")); let mut deps = dummy_init("admin".to_string(), @@ -110,12 +111,12 @@ pub mod tests { oracle, None, Option::from(treasury), - Option::from(commission)); + Option::from(capture)); // new config vars let new_oracle = Option::from(create_contract("new_oracle", "hash")); let new_treasury = Option::from(create_contract("new_treasury", "hash")); - let new_commission = Option::from(Uint128(200)); + let new_capture = Option::from(Uint128(200)); // Update config let update_msg = HandleMsg::UpdateConfig { @@ -123,7 +124,7 @@ pub mod tests { oracle: new_oracle.clone(), treasury: new_treasury.clone(), // 2% - commission: new_commission.clone(), + capture: new_capture.clone(), }; let update_res = handle(&mut deps, admin_env, update_msg); @@ -133,7 +134,7 @@ pub mod tests { QueryAnswer::Config { config } => { assert_eq!(config.oracle, new_oracle.unwrap()); assert_eq!(config.treasury, new_treasury); - assert_eq!(config.commission, new_commission); + assert_eq!(config.capture, new_capture); } _ => { panic!("Received wrong answer") } } @@ -175,7 +176,7 @@ pub mod tests { create_contract("", ""), create_contract("", ""), None, - None, + None, None); // Admin should be allowed to add an item @@ -365,14 +366,13 @@ pub mod tests { } } */ - #[test] - fn commission_calc() { + fn capture_calc() { let amount = Uint128(1_000_000_000_000_000_000); //10% - let commission = Uint128(1000); + let capture = Uint128(1000); let expected = Uint128(100_000_000_000_000_000); - let value = calculate_commission(amount, commission); + let value = calculate_capture(amount, capture); assert_eq!(value, expected); } #[test] diff --git a/contracts/mint/Cargo.toml b/contracts/mint/Cargo.toml index b72f053da..1c84b2afb 100644 --- a/contracts/mint/Cargo.toml +++ b/contracts/mint/Cargo.toml @@ -15,17 +15,6 @@ exclude = [ [lib] crate-type = ["cdylib", "rlib"] -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] default = [] # for quicker tests, cargo test --lib @@ -34,10 +23,10 @@ backtraces = ["cosmwasm-std/backtraces"] debug-print = ["cosmwasm-std/debug-print"] [dependencies] -cosmwasm-schema = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-std = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-storage = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -secret-toolkit = { git = "https://github.com/enigmampc/secret-toolkit", branch = "debug-print"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/mint/examples/schema.rs b/contracts/mint/examples/schema.rs deleted file mode 100644 index 99a11865b..000000000 --- a/contracts/mint/examples/schema.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::env::current_dir; -use std::fs::create_dir_all; - -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; - -use mint::msg::{SupportedAssetsResponse, AssetResponse, HandleMsg, InitMsg, QueryMsg}; -use mint::state::{MintConfig, SupportedAsset}; - -fn main() { - let mut out_dir = current_dir().unwrap(); - out_dir.push("schema"); - create_dir_all(&out_dir).unwrap(); - remove_schemas(&out_dir).unwrap(); - - export_schema(&schema_for!(InitMsg), &out_dir); - export_schema(&schema_for!(HandleMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); - export_schema(&schema_for!(MintConfig), &out_dir); - export_schema(&schema_for!(SupportedAsset), &out_dir); - export_schema(&schema_for!(SupportedAssetsResponse), &out_dir); - export_schema(&schema_for!(AssetResponse), &out_dir); -} diff --git a/contracts/mint/rustfmt.toml b/contracts/mint/rustfmt.toml deleted file mode 100644 index 11a85e6a9..000000000 --- a/contracts/mint/rustfmt.toml +++ /dev/null @@ -1,15 +0,0 @@ -# stable -newline_style = "unix" -hard_tabs = false -tab_spaces = 4 - -# unstable... should we require `rustup run nightly cargo fmt` ? -# or just update the style guide when they are stable? -#fn_single_line = true -#format_code_in_doc_comments = true -#overflow_delimited_expr = true -#reorder_impl_items = true -#struct_field_align_threshold = 20 -#struct_lit_single_line = true -#report_todo = "Always" - diff --git a/contracts/mint/schema/asset.json b/contracts/mint/schema/asset.json deleted file mode 100644 index cf4d9b949..000000000 --- a/contracts/mint/schema/asset.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Asset", - "type": "object", - "required": [ - "burned_tokens", - "code_hash", - "contract" - ], - "properties": { - "burned_tokens": { - "$ref": "#/definitions/Uint128" - }, - "code_hash": { - "type": "string" - }, - "contract": { - "$ref": "#/definitions/HumanAddr" - } - }, - "definitions": { - "HumanAddr": { - "type": "string" - }, - "Uint128": { - "type": "string" - } - } -} diff --git a/contracts/mint/schema/asset_response.json b/contracts/mint/schema/asset_response.json deleted file mode 100644 index f74a6b34d..000000000 --- a/contracts/mint/schema/asset_response.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AssetResponse", - "type": "object", - "required": [ - "asset" - ], - "properties": { - "asset": { - "$ref": "#/definitions/Asset" - } - }, - "definitions": { - "Asset": { - "type": "object", - "required": [ - "burned_tokens", - "code_hash", - "contract" - ], - "properties": { - "burned_tokens": { - "$ref": "#/definitions/Uint128" - }, - "code_hash": { - "type": "string" - }, - "contract": { - "$ref": "#/definitions/HumanAddr" - } - } - }, - "HumanAddr": { - "type": "string" - }, - "Uint128": { - "type": "string" - } - } -} diff --git a/contracts/mint/schema/config.json b/contracts/mint/schema/config.json deleted file mode 100644 index a39eab5f0..000000000 --- a/contracts/mint/schema/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Config", - "type": "object", - "required": [ - "oracle_contract", - "oracle_contract_code_hash", - "owner", - "silk_contract", - "silk_contract_code_hash" - ], - "properties": { - "oracle_contract": { - "$ref": "#/definitions/HumanAddr" - }, - "oracle_contract_code_hash": { - "type": "string" - }, - "owner": { - "$ref": "#/definitions/HumanAddr" - }, - "silk_contract": { - "$ref": "#/definitions/HumanAddr" - }, - "silk_contract_code_hash": { - "type": "string" - } - }, - "definitions": { - "HumanAddr": { - "type": "string" - } - } -} diff --git a/contracts/mint/schema/handle_msg.json b/contracts/mint/schema/handle_msg.json deleted file mode 100644 index 07181e1e3..000000000 --- a/contracts/mint/schema/handle_msg.json +++ /dev/null @@ -1,490 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "HandleMsg", - "anyOf": [ - { - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "oracle_contract", - "oracle_contract_code_hash", - "owner", - "silk_contract", - "silk_contract_code_hash" - ], - "properties": { - "oracle_contract": { - "$ref": "#/definitions/HumanAddr" - }, - "oracle_contract_code_hash": { - "type": "string" - }, - "owner": { - "$ref": "#/definitions/HumanAddr" - }, - "silk_contract": { - "$ref": "#/definitions/HumanAddr" - }, - "silk_contract_code_hash": { - "type": "string" - } - } - } - } - }, - { - "type": "object", - "required": [ - "register_asset" - ], - "properties": { - "register_asset": { - "type": "object", - "required": [ - "code_hash", - "contract" - ], - "properties": { - "code_hash": { - "type": "string" - }, - "contract": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - }, - { - "type": "object", - "required": [ - "update_asset" - ], - "properties": { - "update_asset": { - "type": "object", - "required": [ - "asset", - "code_hash", - "contract" - ], - "properties": { - "asset": { - "$ref": "#/definitions/HumanAddr" - }, - "code_hash": { - "type": "string" - }, - "contract": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - }, - { - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "type": "object", - "required": [ - "amount", - "from", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "from": { - "$ref": "#/definitions/HumanAddr" - }, - "msg": { - "anyOf": [ - { - "$ref": "#/definitions/CosmosMsg_for_Empty" - }, - { - "type": "null" - } - ] - }, - "sender": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - } - ], - "definitions": { - "BankMsg": { - "anyOf": [ - { - "type": "object", - "required": [ - "send" - ], - "properties": { - "send": { - "type": "object", - "required": [ - "amount", - "from_address", - "to_address" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "from_address": { - "$ref": "#/definitions/HumanAddr" - }, - "to_address": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec", - "type": "string" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "CosmosMsg_for_Empty": { - "anyOf": [ - { - "type": "object", - "required": [ - "bank" - ], - "properties": { - "bank": { - "$ref": "#/definitions/BankMsg" - } - } - }, - { - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - } - }, - { - "type": "object", - "required": [ - "staking" - ], - "properties": { - "staking": { - "$ref": "#/definitions/StakingMsg" - } - } - }, - { - "type": "object", - "required": [ - "wasm" - ], - "properties": { - "wasm": { - "$ref": "#/definitions/WasmMsg" - } - } - }, - { - "type": "object", - "required": [ - "gov" - ], - "properties": { - "gov": { - "$ref": "#/definitions/GovMsg" - } - } - } - ] - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "GovMsg": { - "anyOf": [ - { - "type": "object", - "required": [ - "vote" - ], - "properties": { - "vote": { - "type": "object", - "required": [ - "proposal", - "vote_option" - ], - "properties": { - "proposal": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "vote_option": { - "$ref": "#/definitions/VoteOption" - } - } - } - } - } - ] - }, - "HumanAddr": { - "type": "string" - }, - "StakingMsg": { - "anyOf": [ - { - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - }, - { - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - }, - { - "type": "object", - "required": [ - "withdraw" - ], - "properties": { - "withdraw": { - "type": "object", - "required": [ - "validator" - ], - "properties": { - "recipient": { - "description": "this is the \"withdraw address\", the one that should receive the rewards if None, then use delegator address", - "anyOf": [ - { - "$ref": "#/definitions/HumanAddr" - }, - { - "type": "null" - } - ] - }, - "validator": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - }, - { - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "$ref": "#/definitions/HumanAddr" - }, - "src_validator": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - } - ] - }, - "Uint128": { - "type": "string" - }, - "VoteOption": { - "type": "string", - "enum": [ - "Yes", - "No", - "Abstain", - "NoWithVeto" - ] - }, - "WasmMsg": { - "anyOf": [ - { - "description": "this dispatches a call to another contract at a known address (with known ABI)", - "type": "object", - "required": [ - "execute" - ], - "properties": { - "execute": { - "type": "object", - "required": [ - "callback_code_hash", - "contract_addr", - "msg", - "send" - ], - "properties": { - "callback_code_hash": { - "description": "callback_code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", - "type": "string" - }, - "contract_addr": { - "$ref": "#/definitions/HumanAddr" - }, - "msg": { - "description": "msg is the json-encoded HandleMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "send": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - } - } - }, - { - "description": "this instantiates a new contracts from previously uploaded wasm code", - "type": "object", - "required": [ - "instantiate" - ], - "properties": { - "instantiate": { - "type": "object", - "required": [ - "callback_code_hash", - "code_id", - "label", - "msg", - "send" - ], - "properties": { - "callback_code_hash": { - "description": "callback_code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", - "type": "string" - }, - "code_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "label": { - "description": "mandatory human-readbale label for the contract", - "type": "string" - }, - "msg": { - "description": "msg is the json-encoded InitMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "send": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - } - } - } - ] - } - } -} diff --git a/contracts/mint/schema/init_msg.json b/contracts/mint/schema/init_msg.json deleted file mode 100644 index 7a9f29da9..000000000 --- a/contracts/mint/schema/init_msg.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InitMsg", - "type": "object", - "required": [ - "oracle_contract", - "oracle_contract_code_hash", - "silk_contract", - "silk_contract_code_hash" - ], - "properties": { - "oracle_contract": { - "$ref": "#/definitions/HumanAddr" - }, - "oracle_contract_code_hash": { - "type": "string" - }, - "silk_contract": { - "$ref": "#/definitions/HumanAddr" - }, - "silk_contract_code_hash": { - "type": "string" - } - }, - "definitions": { - "HumanAddr": { - "type": "string" - } - } -} diff --git a/contracts/mint/schema/query_msg.json b/contracts/mint/schema/query_msg.json deleted file mode 100644 index 73e8d9378..000000000 --- a/contracts/mint/schema/query_msg.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "anyOf": [ - { - "type": "object", - "required": [ - "get_supported_assets" - ], - "properties": { - "get_supported_assets": { - "type": "object" - } - } - }, - { - "type": "object", - "required": [ - "get_asset" - ], - "properties": { - "get_asset": { - "type": "object", - "required": [ - "contract" - ], - "properties": { - "contract": { - "type": "string" - } - } - } - } - } - ] -} diff --git a/contracts/mint/schema/supported_assets_response.json b/contracts/mint/schema/supported_assets_response.json deleted file mode 100644 index dd63512ae..000000000 --- a/contracts/mint/schema/supported_assets_response.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SupportedAssetsResponse", - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "type": "string" - } - } - } -} diff --git a/contracts/mint/src/contract.rs b/contracts/mint/src/contract.rs index f49ca55f9..0639ea32c 100644 --- a/contracts/mint/src/contract.rs +++ b/contracts/mint/src/contract.rs @@ -1,19 +1,43 @@ -use cosmwasm_std::{debug_print, to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, StdError, StdResult, Storage, CosmosMsg, HumanAddr, Uint128, from_binary}; -use crate::state::{config, config_read, assets_w, assets_r, asset_list, asset_list_read}; +use crate::state::{asset_list, asset_list_read, assets_r, assets_w, config, config_read}; +use cosmwasm_std::{ + debug_print, + from_binary, + to_binary, + Api, + Binary, + CosmosMsg, + Env, + Extern, + HandleResponse, + HumanAddr, + InitResponse, + Querier, + StdError, + StdResult, + Storage, + Uint128, +}; use secret_toolkit::{ - snip20::{mint_msg, burn_msg, register_receive_msg, token_info_query, minters_query}, - utils::{InitCallback, Query} + snip20::{burn_msg, mint_msg, minters_query, register_receive_msg, token_info_query}, + utils::{InitCallback, Query}, }; use shade_protocol::{ - mint::{InitMsg, HandleMsg, HandleAnswer, QueryMsg, QueryAnswer, MintConfig, SupportedAsset, SnipMsgHook}, - oracle::{ - QueryMsg::GetPrice, - }, + asset::Contract, band::ReferenceData, - asset::{Contract}, generic_response::ResponseStatus, + mint::{ + HandleAnswer, + HandleMsg, + InitMsg, + MintConfig, + QueryAnswer, + QueryMsg, + SnipMsgHook, + SupportedAsset, + }, + oracle::QueryMsg::Price, }; -use std::convert::TryFrom; +use std::{cmp::Ordering, convert::TryFrom}; // TODO: add remove asset // TODO: add spacepad padding @@ -24,8 +48,8 @@ pub fn init( ) -> StdResult { let state = MintConfig { owner: match msg.admin { - None => { env.message.sender.clone() } - Some(admin) => { admin } + None => env.message.sender.clone(), + Some(admin) => admin, }, oracle: msg.oracle, activated: true, @@ -46,7 +70,7 @@ pub fn init( Ok(InitResponse { messages, - log: vec![] + log: vec![], }) } @@ -59,12 +83,9 @@ pub fn handle( HandleMsg::Migrate { code_id, code_hash, - label + label, } => try_migrate(deps, env, label, code_id, code_hash), - HandleMsg::UpdateConfig { - owner, - oracle - } => try_update_config(deps, env, owner, oracle), + HandleMsg::UpdateConfig { owner, oracle } => try_update_config(deps, env, owner, oracle), HandleMsg::RegisterAsset { name, contract, @@ -76,7 +97,8 @@ pub fn handle( from, amount, msg, - ..} => try_burn(deps, env, sender, from, amount, msg), + .. + } => try_burn(deps, env, sender, from, amount, msg), } } @@ -106,20 +128,21 @@ pub fn try_migrate( if let Some(item) = assets.may_load(asset_addr.as_bytes())? { initial_assets.push(item) } - }; + } // Move config let init_msg = InitMsg { admin: Option::from(config_read.owner), oracle: config_read.oracle, - initial_assets: Some(initial_assets) + initial_assets: Some(initial_assets), }; Ok(HandleResponse { messages: vec![init_msg.to_cosmos_msg(label, code_id, code_hash, None)?], log: vec![], - data: Some( to_binary( &HandleAnswer::Migrate { - status: ResponseStatus::Success } )? ) + data: Some(to_binary(&HandleAnswer::Migrate { + status: ResponseStatus::Success, + })?), }) } @@ -148,8 +171,9 @@ pub fn try_update_config( Ok(HandleResponse { messages: vec![], log: vec![], - data: Some( to_binary( &HandleAnswer::UpdateConfig { - status: ResponseStatus::Success } )? ) + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), }) } @@ -161,35 +185,39 @@ pub fn try_register_asset( burnable: Option, total_burned: Option, ) -> StdResult { - let asset = SupportedAsset { name: match name { - None => { token_info_query(&deps.querier, 1, contract.code_hash.clone(), contract.address.clone())?.symbol } + None => { + token_info_query( + &deps.querier, + 1, + contract.code_hash.clone(), + contract.address.clone(), + )? + .symbol + } Some(x) => x, }, contract, - burnable: match burnable { - None => false, - Some(x) => x - }, + burnable: burnable.unwrap_or(false), total_burned: match total_burned { None => Uint128(0), Some(amount) => amount, - } + }, }; - if !authorized(deps, &env, AllowedAccess::Admin)? { + if !authorized(deps, env, AllowedAccess::Admin)? { return Err(StdError::Unauthorized { backtrace: None }); } - let mut messages = vec![]; - messages.push(save_asset(deps, env, asset)?); + let messages = vec![save_asset(deps, env, asset)?]; Ok(HandleResponse { messages, log: vec![], - data: Some( to_binary( &HandleAnswer::RegisterAsset { - status: ResponseStatus::Success } )? ) + data: Some(to_binary(&HandleAnswer::RegisterAsset { + status: ResponseStatus::Success, + })?), }) } @@ -199,7 +227,7 @@ pub fn try_burn( _sender: HumanAddr, from: HumanAddr, amount: Uint128, - msg: Option + msg: Option, ) -> StdResult { if !authorized(deps, &env, AllowedAccess::User)? { return Err(StdError::Unauthorized { backtrace: None }); @@ -216,48 +244,78 @@ pub fn try_burn( let assets = assets_r(&deps.storage); let burning_asset = match assets.may_load(env.message.sender.to_string().as_bytes())? { Some(asset) => asset, - None => return Err(StdError::NotFound { kind: env.message.sender.to_string(), backtrace: None }), + None => { + return Err(StdError::NotFound { + kind: env.message.sender.to_string(), + backtrace: None, + }); + } }; let minting_asset = match assets.may_load(msgs.to_mint.to_string().as_bytes())? { Some(asset) => asset, - None => return Err(StdError::NotFound { kind: msgs.to_mint.to_string(), backtrace: None }), + None => { + return Err(StdError::NotFound { + kind: msgs.to_mint.to_string(), + backtrace: None, + }); + } }; // Check that requested snip20 is supported and mint address is inside the mintable array - let mintable = minters_query(&deps.querier, 1, - minting_asset.contract.code_hash.clone(), - minting_asset.contract.address.clone())?.minters; + let mintable = minters_query( + &deps.querier, + 1, + minting_asset.contract.code_hash.clone(), + minting_asset.contract.address.clone(), + )? + .minters; if !mintable.contains(&env.contract.address) { - return Err(StdError::generic_err("Asset does allow mint contract to mint")) + return Err(StdError::generic_err( + "Asset does allow mint contract to mint", + )); } // Query prices - let in_price = call_oracle(&deps, burning_asset.name)?; - let target_price = call_oracle(&deps, minting_asset.name)?; + let in_price = call_oracle(deps, burning_asset.name)?; + let target_price = call_oracle(deps, minting_asset.name)?; // Get asset decimals // Load the decimal information for both coins - let in_decimals = token_info_query(&deps.querier, 1, - burning_asset.contract.code_hash.clone(), - burning_asset.contract.address.clone())?.decimals as u32; - let target_decimals = token_info_query(&deps.querier, 1, - minting_asset.contract.code_hash.clone(), - minting_asset.contract.address.clone())?.decimals as u32; + let in_decimals = token_info_query( + &deps.querier, + 1, + burning_asset.contract.code_hash.clone(), + burning_asset.contract.address.clone(), + )? + .decimals as u32; + let target_decimals = token_info_query( + &deps.querier, + 1, + minting_asset.contract.code_hash.clone(), + minting_asset.contract.address.clone(), + )? + .decimals as u32; // Calculate value to mint - let amount_to_mint = calculate_mint(in_price, target_price, amount, in_decimals, target_decimals); + let amount_to_mint = + calculate_mint(in_price, target_price, amount, in_decimals, target_decimals); // If minimum amount is greater then ignore the process if msgs.minimum_expected_amount > amount_to_mint { - return Err(StdError::generic_err("did not exceed expected amount")) + return Err(StdError::generic_err("did not exceed expected amount")); } // if burnable then burn if not ignore if burning_asset.burnable { - messages.push(burn_msg(amount, None, 256, - burning_asset.contract.code_hash, - burning_asset.contract.address)?); + messages.push(burn_msg( + amount, + None, + None, + 256, + burning_asset.contract.code_hash, + burning_asset.contract.address, + )?); } // Set burned amount @@ -269,24 +327,30 @@ pub fn try_burn( })?; // Mint - messages.push(mint_msg(from, amount_to_mint, None, 256, - minting_asset.contract.code_hash, - minting_asset.contract.address)?); + messages.push(mint_msg( + from, + amount_to_mint, + None, + None, + 256, + minting_asset.contract.code_hash, + minting_asset.contract.address, + )?); Ok(HandleResponse { messages, log: vec![], - data: Some( to_binary( &HandleAnswer::Burn { + data: Some(to_binary(&HandleAnswer::Burn { status: ResponseStatus::Success, - mint_amount: amount_to_mint - } )? ), + mint_amount: amount_to_mint, + })?), }) } // Helper functions #[derive(PartialEq)] -pub enum AllowedAccess{ +pub enum AllowedAccess { Admin, User, } @@ -299,34 +363,35 @@ fn authorized( let config = config_read(&deps.storage).load()?; // Check if contract is still activated if !config.activated { - return Ok(false) + return Ok(false); } if access == AllowedAccess::Admin { // Check if admin if env.message.sender != config.owner { - return Ok(false) + return Ok(false); } } - return Ok(true) + Ok(true) } -fn register_receive ( - env: &Env, - contract: Contract -) -> StdResult { - let cosmos_msg = register_receive_msg( +fn register_receive(env: &Env, contract: Contract) -> StdResult { + register_receive_msg( env.contract_code_hash.clone(), None, 256, contract.code_hash, contract.address, - ); - - cosmos_msg + ) } -fn calculate_mint(in_price: Uint128, target_price: Uint128, in_amount: Uint128, in_decimals: u32, target_decimals: u32) -> Uint128 { +fn calculate_mint( + in_price: Uint128, + target_price: Uint128, + in_amount: Uint128, + in_decimals: u32, + target_decimals: u32, +) -> Uint128 { // Math must only be made in integers // in_decimals = x // target_decimals = y @@ -347,23 +412,22 @@ fn calculate_mint(in_price: Uint128, target_price: Uint128, in_amount: Uint128, let difference: i32 = target_decimals as i32 - in_decimals as i32; // To avoid a mess of different types doing math - if difference < 0 { - in_total.multiply_ratio(1u128, 10u128.pow(u32::try_from(difference.abs()).unwrap())) - } - else if difference > 0 { - Uint128(in_total.u128() * 10u128.pow(u32::try_from(difference).unwrap())) - } - else { - in_total + match difference.cmp(&0) { + Ordering::Greater => { + Uint128(in_total.u128() * 10u128.pow(u32::try_from(difference).unwrap())) + } + Ordering::Less => { + in_total.multiply_ratio(1u128, 10u128.pow(u32::try_from(difference.abs()).unwrap())) + } + Ordering::Equal => in_total, } } -fn save_asset ( +fn save_asset( deps: &mut Extern, env: &Env, asset: SupportedAsset, ) -> StdResult { - let mut assets = assets_w(&mut deps.storage); // Save the asset @@ -386,12 +450,13 @@ fn call_oracle( deps: &Extern, symbol: String, ) -> StdResult { - let block_size = 1; //update this later let config = config_read(&deps.storage).load()?; - let query_msg = GetPrice { symbol }; - let answer: ReferenceData = query_msg.query(&deps.querier, - config.oracle.code_hash, - config.oracle.address)?; + let query_msg = Price { symbol }; + let answer: ReferenceData = query_msg.query( + &deps.querier, + config.oracle.code_hash, + config.oracle.address, + )?; Ok(answer.rate) } @@ -406,36 +471,52 @@ pub fn query( } } -fn query_supported_assets(deps: &Extern) -> StdResult { - Ok(QueryAnswer::SupportedAssets { assets: asset_list_read(&deps.storage).load()? }) +fn query_supported_assets( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::SupportedAssets { + assets: asset_list_read(&deps.storage).load()?, + }) } -fn query_asset(deps: &Extern, contract: String) -> StdResult { +fn query_asset( + deps: &Extern, + contract: String, +) -> StdResult { let assets = assets_r(&deps.storage); return match assets.may_load(contract.as_bytes())? { Some(asset) => Ok(QueryAnswer::Asset { asset }), - None => Err(StdError::NotFound { kind: contract, backtrace: None }), + None => Err(StdError::NotFound { + kind: contract, + backtrace: None, + }), }; } fn query_config(deps: &Extern) -> StdResult { - Ok(QueryAnswer::Config { config: config_read(&deps.storage).load()? }) + Ok(QueryAnswer::Config { + config: config_read(&deps.storage).load()?, + }) } #[cfg(test)] mod tests { use super::*; - use cosmwasm_std::testing::{mock_dependencies, mock_env, MockStorage, MockApi, MockQuerier}; - use cosmwasm_std::{coins, from_binary, StdError}; + use cosmwasm_std::{ + coins, + from_binary, + testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}, + StdError, + }; use shade_protocol::mint::QueryAnswer; fn create_contract(address: &str, code_hash: &str) -> Contract { let env = mock_env(address.to_string(), &[]); - return Contract{ + return Contract { address: env.message.sender, - code_hash: code_hash.to_string() - } + code_hash: code_hash.to_string(), + }; } fn dummy_init(admin: String, oracle: Contract) -> Extern { @@ -443,12 +524,12 @@ mod tests { let msg = InitMsg { admin: None, oracle, - initial_assets: None + initial_assets: None, }; let env = mock_env(admin, &coins(1000, "earth")); let _res = init(&mut deps, env, msg).unwrap(); - return deps + return deps; } #[test] @@ -458,12 +539,12 @@ mod tests { let msg = InitMsg { admin: None, oracle: create_contract("", ""), - initial_assets: Some(vec![SupportedAsset{ + initial_assets: Some(vec![SupportedAsset { name: "some_asset".to_string(), contract: some_contract, burnable: false, - total_burned: Uint128(0) - }]) + total_burned: Uint128(0), + }]), }; let env = mock_env("creator", &coins(1000, "earth")); @@ -486,7 +567,9 @@ mod tests { QueryAnswer::Config { config } => { assert_eq!(config.oracle, oracle_contract); } - _ => { panic!("Received wrong answer") } + _ => { + panic!("Received wrong answer") + } } // Update config @@ -505,17 +588,16 @@ mod tests { match value { QueryAnswer::Config { config } => { assert_eq!(config.oracle, new_oracle_contract); - } - _ => { panic!("Received wrong answer") } + _ => { + panic!("Received wrong answer") + } } - } #[test] fn user_register_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", "")); + let mut deps = dummy_init("admin".to_string(), create_contract("", "")); // User should not be allowed to add an item let user_env = mock_env("user", &coins(1000, "earth")); @@ -524,7 +606,7 @@ mod tests { name: Some("asset".to_string()), contract: dummy_contract, burnable: None, - total_burned: None + total_burned: None, }; let res = handle(&mut deps, user_env, msg); match res { @@ -536,15 +618,18 @@ mod tests { let res = query(&deps, QueryMsg::GetSupportedAssets {}).unwrap(); let value: QueryAnswer = from_binary(&res).unwrap(); match value { - QueryAnswer::SupportedAssets { assets } => { assert_eq!(0, assets.len()) } - _ => { panic!("Expected empty array") } + QueryAnswer::SupportedAssets { assets } => { + assert_eq!(0, assets.len()) + } + _ => { + panic!("Expected empty array") + } } } #[test] fn admin_register_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", "")); + let mut deps = dummy_init("admin".to_string(), create_contract("", "")); // Admin should be allowed to add an item let env = mock_env("admin", &coins(1000, "earth")); @@ -553,7 +638,7 @@ mod tests { name: Some("asset".to_string()), contract: dummy_contract, burnable: None, - total_burned: None + total_burned: None, }; let _res = handle(&mut deps, env, msg).unwrap(); @@ -561,15 +646,18 @@ mod tests { let res = query(&deps, QueryMsg::GetSupportedAssets {}).unwrap(); let value: QueryAnswer = from_binary(&res).unwrap(); match value { - QueryAnswer::SupportedAssets { assets } => { assert_eq!(1, assets.len()) } - _ => { panic!("Received wrong answer") } + QueryAnswer::SupportedAssets { assets } => { + assert_eq!(1, assets.len()) + } + _ => { + panic!("Received wrong answer") + } } } #[test] fn admin_update_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", "")); + let mut deps = dummy_init("admin".to_string(), create_contract("", "")); // Add a supported asset let env = mock_env("admin", &coins(1000, "earth")); @@ -578,7 +666,7 @@ mod tests { name: Some("old_asset".to_string()), contract: dummy_contract, burnable: None, - total_burned: None + total_burned: None, }; let _res = handle(&mut deps, env, msg).unwrap(); @@ -589,16 +677,23 @@ mod tests { name: Some("new_asset".to_string()), contract: dummy_contract, burnable: None, - total_burned: None + total_burned: None, }; let _res = handle(&mut deps, env, msg).unwrap(); // Response should be new dummy contract - let res = query(&deps, QueryMsg::GetAsset { contract: "some_contract".to_string() }).unwrap(); + let res = query(&deps, QueryMsg::GetAsset { + contract: "some_contract".to_string(), + }) + .unwrap(); let value: QueryAnswer = from_binary(&res).unwrap(); match value { - QueryAnswer::Asset { asset } => { assert_eq!("new_asset".to_string(), asset.name) } - _ => { panic!("Received wrong answer") } + QueryAnswer::Asset { asset } => { + assert_eq!("new_asset".to_string(), asset.name) + } + _ => { + panic!("Received wrong answer") + } }; } diff --git a/contracts/mint/src/lib.rs b/contracts/mint/src/lib.rs index 18d548e3f..f30cca474 100644 --- a/contracts/mint/src/lib.rs +++ b/contracts/mint/src/lib.rs @@ -5,7 +5,12 @@ pub mod state; mod wasm { use super::contract; use cosmwasm_std::{ - do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, }; #[no_mangle] diff --git a/contracts/mint/src/state.rs b/contracts/mint/src/state.rs index cecf77216..da9022bbd 100644 --- a/contracts/mint/src/state.rs +++ b/contracts/mint/src/state.rs @@ -1,8 +1,15 @@ -use cosmwasm_std::{Storage}; -use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton, bucket, Bucket, bucket_read, ReadonlyBucket}; -use shade_protocol::{ - mint::{MintConfig, SupportedAsset}, +use cosmwasm_std::Storage; +use cosmwasm_storage::{ + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, }; +use shade_protocol::mint::{MintConfig, SupportedAsset}; pub static CONFIG_KEY: &[u8] = b"config"; pub static NATIVE_COIN_KEY: &[u8] = b"native_coin"; @@ -25,10 +32,10 @@ pub fn asset_list_read(storage: &S) -> ReadonlySingleton(storage: & S) -> ReadonlyBucket { +pub fn assets_r(storage: &S) -> ReadonlyBucket { bucket_read(ASSET_KEY, storage) } pub fn assets_w(storage: &mut S) -> Bucket { bucket(ASSET_KEY, storage) -} \ No newline at end of file +} diff --git a/contracts/mock_band/Cargo.toml b/contracts/mock_band/Cargo.toml index 793b6daab..de88a8eac 100644 --- a/contracts/mock_band/Cargo.toml +++ b/contracts/mock_band/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mock_band" version = "0.1.0" -authors = ["Jack Swenson "] +authors = ["Jack Swenson ", "Guy Garcia "] edition = "2018" exclude = [ @@ -15,17 +15,6 @@ exclude = [ [lib] crate-type = ["cdylib", "rlib"] -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] default = [] # for quicker tests, cargo test --lib @@ -35,10 +24,10 @@ debug-print = ["cosmwasm-std/debug-print"] [dependencies] bincode = "1.3.1" -cosmwasm-schema = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-std = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-storage = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -secret-toolkit = { git = "https://github.com/enigmampc/secret-toolkit", branch = "debug-print"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/mock_band/rustfmt.toml b/contracts/mock_band/rustfmt.toml deleted file mode 100644 index 11a85e6a9..000000000 --- a/contracts/mock_band/rustfmt.toml +++ /dev/null @@ -1,15 +0,0 @@ -# stable -newline_style = "unix" -hard_tabs = false -tab_spaces = 4 - -# unstable... should we require `rustup run nightly cargo fmt` ? -# or just update the style guide when they are stable? -#fn_single_line = true -#format_code_in_doc_comments = true -#overflow_delimited_expr = true -#reorder_impl_items = true -#struct_field_align_threshold = 20 -#struct_lit_single_line = true -#report_todo = "Always" - diff --git a/contracts/mock_band/src/contract.rs b/contracts/mock_band/src/contract.rs index 9b9506892..bdcf20344 100644 --- a/contracts/mock_band/src/contract.rs +++ b/contracts/mock_band/src/contract.rs @@ -1,14 +1,32 @@ use cosmwasm_std::{ - to_binary, Api, Binary, - Env, Extern, HandleResponse, InitResponse, - Querier, StdResult, StdError, Storage, Uint128, + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdError, + StdResult, + Storage, + Uint128, }; -use serde::{Deserialize, Serialize}; use schemars::JsonSchema; -use shade_protocol::band::ReferenceData; +use serde::{Deserialize, Serialize}; +use shade_protocol::band::{InitMsg, ReferenceData}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct InitMsg { } +use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket}; + +pub static PRICE: &[u8] = b"prices"; + +pub fn price_r(storage: &S) -> ReadonlyBucket { + bucket_read(PRICE, storage) +} + +pub fn price_w(storage: &mut S) -> Bucket { + bucket(PRICE, storage) +} pub fn init( _deps: &mut Extern, @@ -20,15 +38,21 @@ pub fn init( #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum HandleMsg { } +pub enum HandleMsg { + MockPrice { symbol: String, price: Uint128 }, +} pub fn handle( - _deps: &mut Extern, + deps: &mut Extern, _env: Env, - _msg: HandleMsg, -) -> StdResult { - - Err(StdError::GenericErr { msg: "Not Implemented".to_string(), backtrace: None}) + msg: HandleMsg, +) -> StdResult { + return match msg { + HandleMsg::MockPrice { symbol, price } => { + price_w(&mut deps.storage).save(symbol.as_bytes(), &price)?; + Ok(HandleResponse::default()) + } + }; } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -38,37 +62,50 @@ pub enum QueryMsg { base_symbol: String, quote_symbol: String, }, - GetReferenceDataBulk{ + GetReferenceDataBulk { base_symbols: Vec, quote_symbols: Vec, }, } pub fn query( - _deps: &Extern, + deps: &Extern, msg: QueryMsg, ) -> StdResult { match msg { - QueryMsg::GetReferenceData { base_symbol: _, quote_symbol: _ } => - to_binary(&ReferenceData { - rate: Uint128(1_000_000_000_000_000_000), - last_updated_base: 1628544285u64, - last_updated_quote: 3377610u64 - }), + QueryMsg::GetReferenceData { + base_symbol, + quote_symbol: _, + } => { + if let Some(price) = price_r(&deps.storage).may_load(base_symbol.as_bytes())? { + return to_binary(&ReferenceData { + rate: price, + last_updated_base: 0, + last_updated_quote: 0, + }); + } + Err(StdError::generic_err("Missing Price Feed")) + } QueryMsg::GetReferenceDataBulk { base_symbols, - quote_symbols: _ + quote_symbols: _, } => { let mut results = Vec::new(); - let data = ReferenceData { - rate: Uint128(1_000_000_000_000_000_000), - last_updated_base: 1628544285u64, - last_updated_quote: 3377610u64 - }; - for _ in base_symbols { - results.push(data.clone()); + for sym in base_symbols { + if let Some(price) = price_r(&deps.storage).may_load(sym.as_bytes())? { + results.push(ReferenceData { + rate: price, + last_updated_base: 0, + last_updated_quote: 0, + }); + } else { + return Err(StdError::GenericErr { + msg: "Missing Price Feed".to_string(), + backtrace: None, + }); + } } to_binary(&results) - }, + } } } diff --git a/contracts/mock_band/src/lib.rs b/contracts/mock_band/src/lib.rs index 4d128dcb3..1669817dd 100644 --- a/contracts/mock_band/src/lib.rs +++ b/contracts/mock_band/src/lib.rs @@ -4,7 +4,12 @@ pub mod contract; mod wasm { use super::contract; use cosmwasm_std::{ - do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, }; #[no_mangle] diff --git a/contracts/oracle/Cargo.toml b/contracts/oracle/Cargo.toml index 671581737..bf7a5e818 100644 --- a/contracts/oracle/Cargo.toml +++ b/contracts/oracle/Cargo.toml @@ -18,17 +18,6 @@ exclude = [ [lib] crate-type = ["cdylib", "rlib"] -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] default = [] # for quicker tests, cargo test --lib @@ -38,10 +27,10 @@ debug-print = ["cosmwasm-std/debug-print"] [dependencies] bincode = "1.3.1" -cosmwasm-schema = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-std = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-storage = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -secret-toolkit = { git = "https://github.com/enigmampc/secret-toolkit", branch = "debug-print"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/oracle/README.md b/contracts/oracle/README.md index a3bd4b07b..c215ea336 100644 --- a/contracts/oracle/README.md +++ b/contracts/oracle/README.md @@ -16,8 +16,9 @@ The oracle contract is used to query the price of different currencies |Name |Type |Description | optional | |----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| -|owner | string | New contract owner; SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | yes | -|band | Contract | Band protocol contract | no | +|admin | string | New contract admin; SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | yes | +|sscrt | Contract | sSCRT snip20 token contract | no | +|band | Contract | Band protocol contract | no | ## User @@ -77,7 +78,7 @@ Registers a Secret Swap pair that can then be queried ### Queries -#### GetPrice +#### Price Get asset price according to band protocol or a registered SecretSwap pair ##### Request @@ -104,14 +105,16 @@ Get asset price according to band protocol or a registered SecretSwap pair } ``` -#### GetConfig +#### Config Get the current config +#### Prices +Get prices of list of assets ##### Request |Name |Type |Description | optional | |------------|--------|-----------------------------------------------------------------------------------------------------------------------|----------| - +|symbols | list | list of asset symbols e.g. BTC/ETH/SCRT; | no | ##### Response |Name |Type |Description | optional | @@ -126,11 +129,14 @@ Addresses are fictional. ```json { - "config": { - "owner": "secret1k0jntykt7e4g3y88ltc60czgjhjsd74c9e8fzek", - "band": "secret1k0hdtykt7e4hs6588ltc60czgjhjsd78xse7bsgfe", - "sscrt": "secret1k0hdtyjs75fhs6588ltc60czgjhjsd78xse87hdk", - } + [ + { + "rate": "1470000000000000000", + "last_updated_base": 1628569146, + "last_updated_quote": 3377610 + }, + ... + ] } ``` diff --git a/contracts/oracle/examples/schema.rs b/contracts/oracle/examples/schema.rs deleted file mode 100644 index 9cf01c780..000000000 --- a/contracts/oracle/examples/schema.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::env::current_dir; -use std::fs::create_dir_all; - -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; - -use secure_secret::msg::{CountResponse, HandleMsg, InitMsg, QueryMsg}; -use secure_secret::state::State; - -fn main() { - let mut out_dir = current_dir().unwrap(); - out_dir.push("schema"); - create_dir_all(&out_dir).unwrap(); - remove_schemas(&out_dir).unwrap(); - - export_schema(&schema_for!(InitMsg), &out_dir); - export_schema(&schema_for!(HandleMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); - export_schema(&schema_for!(State), &out_dir); - export_schema(&schema_for!(CountResponse), &out_dir); -} diff --git a/contracts/oracle/rustfmt.toml b/contracts/oracle/rustfmt.toml deleted file mode 100644 index 11a85e6a9..000000000 --- a/contracts/oracle/rustfmt.toml +++ /dev/null @@ -1,15 +0,0 @@ -# stable -newline_style = "unix" -hard_tabs = false -tab_spaces = 4 - -# unstable... should we require `rustup run nightly cargo fmt` ? -# or just update the style guide when they are stable? -#fn_single_line = true -#format_code_in_doc_comments = true -#overflow_delimited_expr = true -#reorder_impl_items = true -#struct_field_align_threshold = 20 -#struct_lit_single_line = true -#report_todo = "Always" - diff --git a/contracts/oracle/schema/count_response.json b/contracts/oracle/schema/count_response.json deleted file mode 100644 index fa1e81f32..000000000 --- a/contracts/oracle/schema/count_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CountResponse", - "type": "object", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "format": "int32" - } - } -} diff --git a/contracts/oracle/schema/handle_msg.json b/contracts/oracle/schema/handle_msg.json deleted file mode 100644 index a9783e3e1..000000000 --- a/contracts/oracle/schema/handle_msg.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "HandleMsg", - "anyOf": [ - { - "type": "object", - "required": [ - "increment" - ], - "properties": { - "increment": { - "type": "object" - } - } - }, - { - "type": "object", - "required": [ - "reset" - ], - "properties": { - "reset": { - "type": "object", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "format": "int32" - } - } - } - } - } - ] -} diff --git a/contracts/oracle/schema/init_msg.json b/contracts/oracle/schema/init_msg.json deleted file mode 100644 index 998bc1ba9..000000000 --- a/contracts/oracle/schema/init_msg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InitMsg", - "type": "object", - "required": [ - "count" - ], - "properties": { - "count": { - "type": "integer", - "format": "int32" - } - } -} diff --git a/contracts/oracle/schema/query_msg.json b/contracts/oracle/schema/query_msg.json deleted file mode 100644 index daf9e90b5..000000000 --- a/contracts/oracle/schema/query_msg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "anyOf": [ - { - "type": "object", - "required": [ - "get_count" - ], - "properties": { - "get_count": { - "type": "object" - } - } - } - ] -} diff --git a/contracts/oracle/schema/state.json b/contracts/oracle/schema/state.json deleted file mode 100644 index 9cd0f1bc9..000000000 --- a/contracts/oracle/schema/state.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "State", - "type": "object", - "required": [ - "count", - "owner" - ], - "properties": { - "count": { - "type": "integer", - "format": "int32" - }, - "owner": { - "$ref": "#/definitions/CanonicalAddr" - } - }, - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec", - "type": "string" - }, - "CanonicalAddr": { - "$ref": "#/definitions/Binary" - } - } -} diff --git a/contracts/oracle/src/contract.rs b/contracts/oracle/src/contract.rs index 096540bfd..2eeef4040 100644 --- a/contracts/oracle/src/contract.rs +++ b/contracts/oracle/src/contract.rs @@ -1,22 +1,18 @@ +use crate::{handle, query, state::config_w}; use cosmwasm_std::{ - debug_print, to_binary, Api, Binary, - Env, Extern, HandleResponse, InitResponse, - Querier, StdResult, StdError, Storage, Uint128, -}; -use shade_protocol::{ - oracle::{ - InitMsg, HandleMsg, - QueryMsg, OracleConfig, - }, - band::ReferenceData, -}; -use crate::{ - state::{ - config_w, config_r, - hard_coded_r, hard_coded_w, - }, - query, handle, + debug_print, + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdResult, + Storage, }; +use shade_protocol::oracle::{HandleMsg, InitMsg, OracleConfig, QueryMsg}; pub fn init( deps: &mut Extern, @@ -24,9 +20,9 @@ pub fn init( msg: InitMsg, ) -> StdResult { let state = OracleConfig { - owner: match msg.admin { - None => { env.message.sender.clone() } - Some(admin) => { admin } + admin: match msg.admin { + None => env.message.sender.clone(), + Some(admin) => admin, }, band: msg.band, sscrt: msg.sscrt, @@ -34,15 +30,6 @@ pub fn init( config_w(&mut deps.storage).save(&state)?; - /* Hard-coded SILK = $1.00 - */ - hard_coded_w(&mut deps.storage).save("SILK".as_bytes(), &ReferenceData { - //1$ - rate: Uint128(1 * 10u128.pow(18)), - last_updated_base: 0, - last_updated_quote: 0 - })?; - debug_print!("Contract was initialized by {}", env.message.sender); Ok(InitResponse::default()) @@ -53,15 +40,15 @@ pub fn handle( env: Env, msg: HandleMsg, ) -> StdResult { - match msg { - HandleMsg::UpdateConfig { - owner, - band, - } => handle::try_update_config(deps, env, owner, band), - HandleMsg::RegisterSswapPair { - pair, - } => handle::register_sswap_pair(deps, env, pair), + HandleMsg::UpdateConfig { admin, band } => { + handle::try_update_config(deps, env, admin, band) + } + HandleMsg::RegisterSswapPair { pair } => handle::register_sswap_pair(deps, env, pair), + HandleMsg::UnregisterSswapPair { pair } => handle::unregister_sswap_pair(deps, env, pair), + HandleMsg::RegisterIndex { symbol, basket } => { + handle::register_index(deps, env, symbol, basket) + } } } @@ -70,7 +57,8 @@ pub fn query( msg: QueryMsg, ) -> StdResult { match msg { - QueryMsg::GetConfig {} => to_binary(&query::config(deps)?), - QueryMsg::GetPrice { symbol } => to_binary(&query::get_price(deps, symbol)?), + QueryMsg::Config {} => to_binary(&query::config(deps)?), + QueryMsg::Price { symbol } => to_binary(&query::price(deps, symbol)?), + QueryMsg::Prices { symbols } => to_binary(&query::prices(deps, symbols)?), } } diff --git a/contracts/oracle/src/handle.rs b/contracts/oracle/src/handle.rs index ee64d01d7..07899a703 100644 --- a/contracts/oracle/src/handle.rs +++ b/contracts/oracle/src/handle.rs @@ -1,35 +1,26 @@ +use crate::state::{config_r, config_w, index_w, sswap_pairs_r, sswap_pairs_w}; use cosmwasm_std::{ - to_binary, Api, - Env, Extern, HandleResponse, - Querier, StdResult, StdError, Storage, + to_binary, + Api, + Env, + Extern, + HandleResponse, HumanAddr, + Querier, + StdError, + StdResult, + Storage, }; use secret_toolkit::{ + snip20::{token_info_query, TokenInfo}, utils::Query, - snip20::{ - token_info_query, - }, }; use shade_protocol::{ - oracle::{ - HandleAnswer, - SswapPair - }, asset::Contract, generic_response::ResponseStatus, - snip20::{ - Snip20Asset, - token_config_query, - TokenConfig, - }, - secretswap::{ - PairQuery, - PairResponse, - } -}; -use crate::state::{ - config_w, config_r, - sswap_pairs_w, + oracle::{HandleAnswer, IndexElement, SswapPair}, + secretswap::{PairQuery, PairResponse}, + snip20::Snip20Asset, }; pub fn register_sswap_pair( @@ -37,74 +28,153 @@ pub fn register_sswap_pair( env: Env, pair: Contract, ) -> StdResult { + let config = config_r(&deps.storage).load()?; + if env.message.sender != config.admin { + return Err(StdError::Unauthorized { backtrace: None }); + } + + let (token_contract, token_info) = + fetch_token_paired_to_sscrt_on_sswap(deps, config.sscrt.address, &pair)?; + sswap_pairs_w(&mut deps.storage).save(token_info.symbol.as_bytes(), &SswapPair { + pair, + asset: Snip20Asset { + contract: token_contract, + token_info: token_info.clone(), + token_config: None, + }, + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::RegisterSswapPair { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn unregister_sswap_pair( + deps: &mut Extern, + env: Env, + pair: Contract, +) -> StdResult { let config = config_r(&deps.storage).load()?; - if env.message.sender != config.owner { + if env.message.sender != config.admin { return Err(StdError::Unauthorized { backtrace: None }); } - //Query for snip20's in the pair - let response: PairResponse = PairQuery::Pair {}.query( - &deps.querier, - pair.code_hash.clone(), - pair.address.clone(), - )?; + let (_, token_info) = fetch_token_paired_to_sscrt_on_sswap(deps, config.sscrt.address, &pair)?; + + sswap_pairs_w(&mut deps.storage).remove(token_info.symbol.as_bytes()); + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UnregisterSswapPair { + status: ResponseStatus::Success, + })?), + }) +} + +/// +/// Will fetch token Contract along with TokenInfo for {symbol} in pair argument. +/// Pair argument must represent Secret Swap contract for {symbol}/sSCRT or sSCRT/{symbol}. +/// +fn fetch_token_paired_to_sscrt_on_sswap( + deps: &mut Extern, + sscrt_addr: HumanAddr, + pair: &Contract, +) -> StdResult<(Contract, TokenInfo)> { + // Query for snip20's in the pair + let response: PairResponse = + PairQuery::Pair {}.query(&deps.querier, pair.code_hash.clone(), pair.address.clone())?; let mut token_contract = Contract { address: response.asset_infos[0].token.contract_addr.clone(), code_hash: response.asset_infos[0].token.token_code_hash.clone(), }; // if thats sscrt, switch it - if token_contract.address == config.sscrt.address { + if token_contract.address == sscrt_addr { token_contract = Contract { address: response.asset_infos[1].token.contract_addr.clone(), code_hash: response.asset_infos[1].token.token_code_hash.clone(), } } // if neither is sscrt - else if response.asset_infos[1].token.contract_addr != config.sscrt.address { - return Err(StdError::NotFound { kind: "Not an SSCRT Pair".to_string(), backtrace: None }); + else if response.asset_infos[1].token.contract_addr != sscrt_addr { + return Err(StdError::NotFound { + kind: "Not an SSCRT Pair".to_string(), + backtrace: None, + }); } - let token_info = token_info_query(&deps.querier, 1, - token_contract.code_hash.clone(), - token_contract.address.clone())?; + let token_info = token_info_query( + &deps.querier, + 1, + token_contract.code_hash.clone(), + token_contract.address.clone(), + )?; - sswap_pairs_w(&mut deps.storage).save(token_info.symbol.as_bytes(), &SswapPair { - pair, - asset: Snip20Asset { - contract: token_contract, - token_info: token_info.clone(), - token_config: None, + Ok((token_contract, token_info)) +} + +pub fn register_index( + deps: &mut Extern, + env: Env, + symbol: String, + basket: Vec, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + if env.message.sender != config.admin { + return Err(StdError::Unauthorized { backtrace: None }); + } + + match sswap_pairs_r(&deps.storage).may_load(symbol.as_bytes())? { + None => {} + Some(_) => { + return Err(StdError::GenericErr { + msg: "symbol collides with an existing SecretSwap Pair".to_string(), + backtrace: None, + }); } + } + + //Dont need this, can just use may_load + /* + indices_w(&mut deps.storage).update(|mut symbols| { + symbols.push(symbol.clone()); + Ok(symbols) })?; + */ + + index_w(&mut deps.storage).save(symbol.as_bytes(), &basket)?; Ok(HandleResponse { messages: vec![], log: vec![], - data: Some( to_binary( &HandleAnswer::RegisterSswapPair { - status: ResponseStatus::Success } )? ) + data: Some(to_binary(&HandleAnswer::RegisterIndex { + status: ResponseStatus::Success, + })?), }) - } pub fn try_update_config( deps: &mut Extern, env: Env, - owner: Option, + admin: Option, band: Option, ) -> StdResult { - let config = config_r(&deps.storage).load()?; - if env.message.sender != config.owner { + if env.message.sender != config.admin { return Err(StdError::Unauthorized { backtrace: None }); } // Save new info let mut config = config_w(&mut deps.storage); config.update(|mut state| { - if let Some(owner) = owner { - state.owner = owner; + if let Some(admin) = admin { + state.admin = admin; } if let Some(band) = band { state.band = band; @@ -116,7 +186,8 @@ pub fn try_update_config( Ok(HandleResponse { messages: vec![], log: vec![], - data: Some( to_binary( &HandleAnswer::UpdateConfig{ - status: ResponseStatus::Success } )? ) + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), }) } diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index 14d4d23b1..84be1cef6 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -1,7 +1,7 @@ pub mod contract; -pub mod state; pub mod handle; pub mod query; +pub mod state; #[cfg(test)] mod test; @@ -10,7 +10,12 @@ mod test; mod wasm { use super::contract; use cosmwasm_std::{ - do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, }; #[no_mangle] diff --git a/contracts/oracle/src/query.rs b/contracts/oracle/src/query.rs index a9b2eae1e..0cea79cca 100644 --- a/contracts/oracle/src/query.rs +++ b/contracts/oracle/src/query.rs @@ -1,67 +1,39 @@ -use cosmwasm_std::{ - Api, - Extern, - Querier, StdResult, Storage, - Uint128, - Env, - StdError, -}; +use crate::state::{config_r, index_r, sswap_pairs_r}; +use cosmwasm_std::{Api, Extern, Querier, StdResult, Storage, Uint128}; use secret_toolkit::utils::Query; use shade_protocol::{ - oracle::{ - QueryMsg, QueryAnswer, SswapPair - }, - band::{ - BandQuery, ReferenceData, - }, - secretswap::{ - PairQuery, - SimulationResponse, - Simulation, - Asset, - AssetInfo, - Token, - }, - -}; -use crate::state::{ - config_r, - hard_coded_r, - sswap_pairs_r, + band::{BandQuery, ReferenceData}, + oracle::{IndexElement, QueryAnswer, SswapPair}, + secretswap::{Asset, AssetInfo, PairQuery, SimulationResponse, Token}, }; use std::convert::TryFrom; pub fn config(deps: &Extern) -> StdResult { - Ok(QueryAnswer::Config { config: config_r(&deps.storage).load()? }) + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) } -pub fn get_price( +pub fn price( deps: &Extern, symbol: String, ) -> StdResult { - if symbol == "SSCRT" { return reference_data(deps, "SCRT".to_string(), "USD".to_string()); } - if let Some(reference_data) = hard_coded_r(&deps.storage).may_load(&symbol.as_bytes())? { - return Ok(reference_data); - } - - // symbol registered sswap pair + // secret swap pair + // TODO: sienna pair if let Some(sswap_pair) = sswap_pairs_r(&deps.storage).may_load(symbol.as_bytes())? { + return sswap_price(deps, sswap_pair); + } - let trade_price = sswap_simulate(&deps, sswap_pair)?; - - let scrt_result = reference_data(deps, "SCRT".to_string(), "USD".to_string())?; - - //return Err(StdError::NotFound { kind: translate_price(scrt_result.rate, trade_price).to_string(), backtrace: None }); - + // Index + if let Some(index) = index_r(&deps.storage).may_load(symbol.as_bytes())? { return Ok(ReferenceData { - // SCRT-USD / SCRT-symbol - rate: translate_price(scrt_result.rate, trade_price), + rate: eval_index(deps, &symbol, index)?, last_updated_base: 0, - last_updated_quote: 0 + last_updated_quote: 0, }); } @@ -69,17 +41,82 @@ pub fn get_price( reference_data(deps, symbol, "USD".to_string()) } +pub fn prices( + deps: &Extern, + symbols: Vec, +) -> StdResult> { + let mut band_symbols = vec![]; + let mut band_quotes = vec![]; + let mut results = vec![Uint128(0); symbols.len()]; + + for (i, sym) in symbols.iter().enumerate() { + if let Some(sswap_pair) = sswap_pairs_r(&deps.storage).may_load(sym.as_bytes())? { + results[i] = sswap_price(deps, sswap_pair)?.rate; + } else if let Some(index) = index_r(&deps.storage).may_load(sym.as_bytes())? { + results[i] = eval_index(deps, sym, index)?; + } else { + band_symbols.push(sym.clone()); + band_quotes.push("USD".to_string()); + } + } + + let ref_data = reference_data_bulk(deps, band_symbols.clone(), band_quotes)?; + + for (data, sym) in ref_data.iter().zip(band_symbols.iter()) { + let result_index = symbols + .iter() + .enumerate() + .find(|&s| *s.1 == *sym) + .unwrap() + .0; + results[result_index] = data.rate; + } + + Ok(results) +} + +pub fn eval_index( + deps: &Extern, + symbol: &str, + index: Vec, +) -> StdResult { + let mut weight_total = Uint128::zero(); + let mut price = Uint128::zero(); + + let mut band_bases = vec![]; + let mut band_quotes = vec![]; + let mut band_weights = vec![]; + + for element in index { + weight_total += element.weight; + + if let Some(sswap_pair) = sswap_pairs_r(&deps.storage).may_load(symbol.as_bytes())? { + price += sswap_price(deps, sswap_pair)? + .rate + .multiply_ratio(element.weight, 10u128.pow(18)); + } else { + band_weights.push(element.weight); + band_bases.push(element.symbol.clone()); + band_quotes.push("USD".to_string()); + } + } + + let ref_data = reference_data_bulk(deps, band_bases, band_quotes)?; + + for (reference, weight) in ref_data.iter().zip(band_weights.iter()) { + price += reference.rate.multiply_ratio(*weight, 10u128.pow(18)); + } + + Ok(price.multiply_ratio(10u128.pow(18), weight_total)) +} + /* Translate price from symbol/sSCRT -> symbol/USD * * scrt_price: SCRT/USD price from BAND * trade_price: SCRT/token trade amount from 1 sSCRT (normalized to price * 10^18) * return: token/USD price */ -pub fn translate_price( - scrt_price: Uint128, - trade_price: Uint128 -) -> Uint128 { - +pub fn translate_price(scrt_price: Uint128, trade_price: Uint128) -> Uint128 { scrt_price.multiply_ratio(10u128.pow(18), trade_price) } @@ -87,21 +124,34 @@ pub fn translate_price( * amount: unsigned quantity received in trade for 1sSCRT * decimals: number of decimals for received snip20 */ -pub fn normalize_price( - amount: Uint128, - decimals: u8 -) -> Uint128 { - +pub fn normalize_price(amount: Uint128, decimals: u8) -> Uint128 { (amount.u128() * 10u128.pow(18u32 - u32::try_from(decimals).unwrap())).into() } // Secret Swap interactions +pub fn sswap_price( + deps: &Extern, + sswap_pair: SswapPair, +) -> StdResult { + let trade_price = sswap_simulate(deps, sswap_pair)?; + + let scrt_result = reference_data(deps, "SCRT".to_string(), "USD".to_string())?; + + //return Err(StdError::NotFound { kind: translate_price(scrt_result.rate, trade_price).to_string(), backtrace: None }); + + Ok(ReferenceData { + // SCRT-USD / SCRT-symbol + rate: translate_price(scrt_result.rate, trade_price), + last_updated_base: 0, + last_updated_quote: 0, + }) +} + pub fn sswap_simulate( deps: &Extern, sswap_pair: SswapPair, ) -> StdResult { - let config = config_r(&deps.storage).load()?; let response: SimulationResponse = PairQuery::Simulation { @@ -112,16 +162,20 @@ pub fn sswap_simulate( contract_addr: config.sscrt.address, token_code_hash: config.sscrt.code_hash, viewing_key: "SecretSwap".to_string(), - } - } - } - }.query( + }, + }, + }, + } + .query( &deps.querier, sswap_pair.pair.code_hash, sswap_pair.pair.address, )?; - return Ok(normalize_price(response.return_amount, sswap_pair.asset.token_info.decimals)); + Ok(normalize_price( + response.return_amount, + sswap_pair.asset.token_info.decimals, + )) } // BAND interactions @@ -131,17 +185,17 @@ pub fn reference_data( base_symbol: String, quote_symbol: String, ) -> StdResult { - let config_r = config_r(&deps.storage).load()?; - Ok(BandQuery::GetReferenceData { - base_symbol, - quote_symbol, - }.query( + BandQuery::GetReferenceData { + base_symbol, + quote_symbol, + } + .query( &deps.querier, config_r.band.code_hash, config_r.band.address, - )?) + ) } pub fn reference_data_bulk( @@ -149,15 +203,15 @@ pub fn reference_data_bulk( base_symbols: Vec, quote_symbols: Vec, ) -> StdResult> { - let config_r = config_r(&deps.storage).load()?; - Ok(BandQuery::GetReferenceDataBulk { - base_symbols, - quote_symbols, - }.query( + BandQuery::GetReferenceDataBulk { + base_symbols, + quote_symbols, + } + .query( &deps.querier, config_r.band.code_hash, config_r.band.address, - )?) + ) } diff --git a/contracts/oracle/src/state.rs b/contracts/oracle/src/state.rs index 6b355bc78..69c95ec71 100644 --- a/contracts/oracle/src/state.rs +++ b/contracts/oracle/src/state.rs @@ -1,22 +1,19 @@ -use cosmwasm_std::{ - Storage -}; +use cosmwasm_std::Storage; use cosmwasm_storage::{ - singleton, singleton_read, - Singleton, ReadonlySingleton, - bucket, bucket_read, - Bucket, ReadonlyBucket -}; -use shade_protocol::{ - oracle::{ OracleConfig, SswapPair }, - band::ReferenceData, - asset::Contract, + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, }; +use shade_protocol::oracle::{IndexElement, OracleConfig, SswapPair}; pub static CONFIG_KEY: &[u8] = b"config"; -pub static HARD_CODED: &[u8] = b"hard_coded"; pub static SSWAP_PAIRS: &[u8] = b"sswap_pairs"; - +pub static INDEX: &[u8] = b"index"; pub fn config_r(storage: &S) -> ReadonlySingleton { singleton_read(storage, CONFIG_KEY) @@ -26,14 +23,6 @@ pub fn config_w(storage: &mut S) -> Singleton { singleton(storage, CONFIG_KEY) } -pub fn hard_coded_r(storage: &S) -> ReadonlyBucket { - bucket_read(HARD_CODED, storage) -} - -pub fn hard_coded_w(storage: &mut S) -> Bucket { - bucket(HARD_CODED, storage) -} - pub fn sswap_pairs_r(storage: &S) -> ReadonlyBucket { bucket_read(SSWAP_PAIRS, storage) } @@ -41,3 +30,11 @@ pub fn sswap_pairs_r(storage: &S) -> ReadonlyBucket { pub fn sswap_pairs_w(storage: &mut S) -> Bucket { bucket(SSWAP_PAIRS, storage) } + +pub fn index_r(storage: &S) -> ReadonlyBucket> { + bucket_read(INDEX, storage) +} + +pub fn index_w(storage: &mut S) -> Bucket> { + bucket(INDEX, storage) +} diff --git a/contracts/oracle/src/test.rs b/contracts/oracle/src/test.rs index d3811d7b7..0db36a1dc 100644 --- a/contracts/oracle/src/test.rs +++ b/contracts/oracle/src/test.rs @@ -1,9 +1,8 @@ #[cfg(test)] mod tests { use crate::query; - use cosmwasm_std::testing::{mock_dependencies, mock_env, MockStorage, MockApi, MockQuerier}; - use cosmwasm_std::{coins, from_binary, Uint128}; - use shade_protocol::asset::Contract; + + use cosmwasm_std::Uint128; macro_rules! normalize_price_tests { ($($name:ident: $value:expr,)*) => { @@ -31,7 +30,7 @@ mod tests { // price * 10^18 Uint128(1_000_000_000_000_000_000) ), - normalize_1: ( + normalize_2: ( // amount of TKN received for 1 sSCRT Uint128(1_000_000), // TKN 6 decimals @@ -56,7 +55,7 @@ mod tests { translate_price_tests! { translate_0: ( // 1.62 USD per SCRT - Uint128( 1_622_110_000_000_000_000), + Uint128( 1_622_110_000_000_000_000), // 1 sSCRT -> sETH Uint128( 1_413_500_852_332_497), // sETH/USD price @@ -64,7 +63,7 @@ mod tests { ), translate_1: ( // 1.62 USD per SCRT - Uint128( 1_622_110_000_000_000_000), + Uint128( 1_622_110_000_000_000_000), // .000425 ETH per sSCRT Uint128( 425_600_000_000_000), // 3811.34 ETH per USD @@ -72,19 +71,19 @@ mod tests { ), translate_2: ( // 1 USD per scrt - Uint128( 1_000_000_000_000_000_000), + Uint128( 1_000_000_000_000_000_000), // 1 sscrt for .1 SHD - Uint128( 100_000_000_000_000_000), + Uint128( 100_000_000_000_000_000), // 10 SHD per USD - Uint128(10_000_000_000_000_000_000), + Uint128(10_000_000_000_000_000_000), ), translate_3: ( // 1 USD per scrt - Uint128( 1_000_000_000_000_000_000), + Uint128( 1_000_000_000_000_000_000), // 1 sscrt for .02 SHD - Uint128( 20_000_000_000_000_000), + Uint128( 20_000_000_000_000_000), // 50 SHD per USD - Uint128(50_000_000_000_000_000_000), + Uint128(50_000_000_000_000_000_000), ), } } diff --git a/contracts/scrt_staking/.cargo/config b/contracts/scrt_staking/.cargo/config new file mode 100644 index 000000000..882fe08f6 --- /dev/null +++ b/contracts/scrt_staking/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/scrt_staking/.circleci/config.yml b/contracts/scrt_staking/.circleci/config.yml new file mode 100644 index 000000000..127e1ae7d --- /dev/null +++ b/contracts/scrt_staking/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +jobs: + build: + docker: + - image: rust:1.43.1 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + - run: + name: Build + command: cargo wasm --locked + - run: + name: Unit tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Integration tests + command: cargo integration-test --locked + - run: + name: Format source code + command: cargo fmt + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in source code and schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + - target/wasm32-unknown-unknown/release/.fingerprint + - target/wasm32-unknown-unknown/release/build + - target/wasm32-unknown-unknown/release/deps + key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} diff --git a/contracts/scrt_staking/Cargo.toml b/contracts/scrt_staking/Cargo.toml new file mode 100644 index 000000000..2d63199c7 --- /dev/null +++ b/contracts/scrt_staking/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "scrt_staking" +version = "0.1.0" +authors = ["Jack Swenson "] +edition = "2018" + +exclude = [ + # 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. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +debug-print = ["cosmwasm-std/debug-print"] + +[dependencies] +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std", features = ["staking"] } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } +shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +snafu = { version = "0.6.3" } diff --git a/contracts/scrt_staking/Makefile b/contracts/scrt_staking/Makefile new file mode 100644 index 000000000..2493c22f4 --- /dev/null +++ b/contracts/scrt_staking/Makefile @@ -0,0 +1,68 @@ +.PHONY: check +check: + cargo check + +.PHONY: clippy +clippy: + cargo clippy + +PHONY: test +test: unit-test + +.PHONY: unit-test +unit-test: + cargo test + +# This is a local build with debug-prints activated. Debug prints only show up +# in the local development chain (see the `start-server` command below) +# and mainnet won't accept contracts built with the feature enabled. +.PHONY: build _build +build: _build compress-wasm +_build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --features="debug-print" + +# This is a build suitable for uploading to mainnet. +# Calls to `debug_print` get removed by the compiler. +.PHONY: build-mainnet _build-mainnet +build-mainnet: _build-mainnet compress-wasm +_build-mainnet: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +# like build-mainnet, but slower and more deterministic +.PHONY: build-mainnet-reproducible +build-mainnet-reproducible: + docker run --rm -v "$$(pwd)":/contract \ + --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/contract/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + enigmampc/secret-contract-optimizer:1.0.3 + +.PHONY: compress-wasm +compress-wasm: + cp ./target/wasm32-unknown-unknown/release/*.wasm ./contract.wasm + @## The following line is not necessary, may work only on linux (extra size optimization) + @# wasm-opt -Os ./contract.wasm -o ./contract.wasm + cat ./contract.wasm | gzip -9 > ./contract.wasm.gz + +.PHONY: schema +schema: + cargo run --example schema + +# Run local development chain with four funded accounts (named a, b, c, and d) +.PHONY: start-server +start-server: # CTRL+C to stop + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1317:1317 \ + -v $$(pwd):/root/code \ + --name secretdev enigmampc/secret-network-sw-dev:v1.0.4-3 + +# This relies on running `start-server` in another console +# You can run other commands on the secretcli inside the dev image +# by using `docker exec secretdev secretcli`. +.PHONY: store-contract-local +store-contract-local: + docker exec secretdev secretcli tx compute store -y --from a --gas 1000000 /root/code/contract.wasm.gz + +.PHONY: clean +clean: + cargo clean + -rm -f ./contract.wasm ./contract.wasm.gz diff --git a/contracts/scrt_staking/README.md b/contracts/scrt_staking/README.md new file mode 100644 index 000000000..8f95041bb --- /dev/null +++ b/contracts/scrt_staking/README.md @@ -0,0 +1,65 @@ +# sSCRT Staking Contract +* [Introduction](#Introduction) +* [Sections](#Sections) + * [Init](#Init) + * [Admin](#Admin) + * Messages + * [UpdateConfig](#UpdateConfig) + * [Receive](#Receive) + * [Unbond](#Unbond) + * [Claim](#Claim) + * Queries + * [GetConfig](#GetConfig) + * [Delegations](#Delegations) + * [Delegation](#Delegation) +# Introduction +The sSCRT Staking contract receives sSCRT, redeems it for SCRT, then stakes it with a validator that falls within the criteria it has been configured with. The configured `treasury` will receive all funds from claiming rewards/unbonding. + +# Sections + +## Init +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|owner | HumanAddr | contract owner/admin; a valid bech32 address; +|treasury | HumanAddre | contract designated to receive all outgoing funds +|sscrt | Contract | sSCRT Snip-20 contract to accept for redemption/staking, all other funds will error +|validator_bounds | ValidatorBounds | criteria defining an acceptable validator to stake with + +## Admin + +### Messages +#### UpdateConfig +Updates the given values +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|owner | HumanAddr | contract owner/admin; a valid bech32 address; +|treasury | HumanAddre | contract designated to receive all outgoing funds +|sscrt | Contract | sSCRT Snip-20 contract to accept for redemption/staking, all other funds will error +|validator_bounds | ValidatorBounds | criteria defining an acceptable validator to stake with + +##### Response +```json +{ + "update_config": { + "status": "success" + } +} +``` + + +### Queries + +#### GetConfig +Gets the contract's configuration variables +##### Response +```json +{ + "config": { + "config": { + "owner": "Owner address", + } + } +} +``` diff --git a/contracts/scrt_staking/src/contract.rs b/contracts/scrt_staking/src/contract.rs new file mode 100644 index 000000000..28d85448d --- /dev/null +++ b/contracts/scrt_staking/src/contract.rs @@ -0,0 +1,99 @@ +use cosmwasm_std::{ + debug_print, + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdResult, + Storage, +}; + +use shade_protocol::scrt_staking::{Config, HandleMsg, InitMsg, QueryMsg}; + +use secret_toolkit::snip20::{register_receive_msg, set_viewing_key_msg}; + +use crate::{ + handle, + query, + state::{config_w, self_address_w, viewing_key_r, viewing_key_w}, +}; + +pub fn init( + deps: &mut Extern, + env: Env, + msg: InitMsg, +) -> StdResult { + let config = Config { + admin: match msg.admin { + None => env.message.sender.clone(), + Some(admin) => admin, + }, + sscrt: msg.sscrt, + treasury: msg.treasury, + validator_bounds: msg.validator_bounds, + }; + + config_w(&mut deps.storage).save(&config)?; + + self_address_w(&mut deps.storage).save(&env.contract.address)?; + viewing_key_w(&mut deps.storage).save(&msg.viewing_key)?; + + debug_print!("Contract was initialized by {}", env.message.sender); + + Ok(InitResponse { + messages: vec![ + set_viewing_key_msg( + viewing_key_r(&deps.storage).load()?, + None, + 1, + config.sscrt.code_hash.clone(), + config.sscrt.address.clone(), + )?, + register_receive_msg( + env.contract_code_hash, + None, + 256, + config.sscrt.code_hash, + config.sscrt.address, + )?, + ], + log: vec![], + }) +} + +pub fn handle( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + match msg { + HandleMsg::Receive { + sender, + from, + amount, + msg, + .. + } => handle::receive(deps, env, sender, from, amount, msg), + HandleMsg::UpdateConfig { admin } => handle::try_update_config(deps, env, admin), + // Begin unbonding of a certain amount of scrt + HandleMsg::Unbond { validator } => handle::unbond(deps, env, validator), + // Collect a completed unbonding/rewards + HandleMsg::Claim { validator } => handle::claim(deps, env, validator), + } +} + +pub fn query( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + match msg { + QueryMsg::GetConfig {} => to_binary(&query::config(deps)?), + // All delegations + QueryMsg::Delegations {} => to_binary(&query::delegations(deps)?), + QueryMsg::Delegation { validator } => to_binary(&query::delegation(deps, validator)?), + } +} diff --git a/contracts/scrt_staking/src/handle.rs b/contracts/scrt_staking/src/handle.rs new file mode 100644 index 000000000..8d0b974af --- /dev/null +++ b/contracts/scrt_staking/src/handle.rs @@ -0,0 +1,206 @@ +use cosmwasm_std::{ + debug_print, + to_binary, + Api, + Binary, + Coin, + CosmosMsg, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StakingMsg, + StdError, + StdResult, + Storage, + Uint128, + Validator, +}; +use secret_toolkit::snip20::redeem_msg; + +use shade_protocol::{ + generic_response::ResponseStatus, + scrt_staking::{HandleAnswer, ValidatorBounds}, +}; + +use crate::state::{config_r, config_w}; + +pub fn receive( + deps: &mut Extern, + env: Env, + _sender: HumanAddr, + _from: HumanAddr, + amount: Uint128, + _msg: Option, +) -> StdResult { + debug_print!("Received {}", amount); + + //TODO: verify sscrt else (fail/send to treasury) + + // Redeem all sscrt for scrt + // Fail if incorrect denom + // Stake all current scrt - unbondings + + let mut messages: Vec = vec![]; + + let config = config_r(&deps.storage).load()?; + + if config.sscrt.address != env.message.sender { + return Err(StdError::GenericErr { + msg: "Only accepts sSCRT".to_string(), + backtrace: None, + }); + } + + // Redeem sSCRT -> SCRT + messages.push(redeem_msg( + amount, + None, + None, + 256, + config.sscrt.code_hash.clone(), + config.sscrt.address, + )?); + + let validator = choose_validator(deps, env.block.time)?; + + messages.push(CosmosMsg::Staking(StakingMsg::Delegate { + validator: validator.address.clone(), + amount: Coin { + amount, + denom: "uscrt".to_string(), + }, + })); + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::Receive { + status: ResponseStatus::Success, + validator, + })?), + }) +} + +pub fn try_update_config( + deps: &mut Extern, + env: Env, + admin: Option, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + if env.message.sender != config.admin { + return Err(StdError::Unauthorized { backtrace: None }); + } + + // Save new info + let mut config = config_w(&mut deps.storage); + config.update(|mut state| { + if let Some(admin) = admin { + state.admin = admin; + } + Ok(state) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn unbond( + deps: &mut Extern, + env: Env, + validator: HumanAddr, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + if env.message.sender != config.admin && env.message.sender != config.treasury { + return Err(StdError::Unauthorized { backtrace: None }); + } + + if let Some(delegation) = deps + .querier + .query_delegation(env.contract.address, validator.clone())? + { + let messages: Vec = vec![CosmosMsg::Staking(StakingMsg::Undelegate { + validator, + amount: delegation.amount.clone(), + })]; + + return Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::Unbond { + status: ResponseStatus::Success, + delegation, + })?), + }); + } + + Err(StdError::GenericErr { + msg: "No delegation".to_string(), + backtrace: None, + }) +} + +/* + * Claims rewards and collects completed unbondings + * from a given validator and returns them directly to treasury + * + * TODO: convert to sSCRT first or rely on treasury to do so + */ +pub fn claim( + deps: &mut Extern, + _env: Env, + validator: HumanAddr, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + Ok(HandleResponse { + messages: vec![CosmosMsg::Staking(StakingMsg::Withdraw { + validator, + recipient: Some(config.treasury), + })], + log: vec![], + data: Some(to_binary(&HandleAnswer::Claim { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn choose_validator( + deps: &Extern, + seed: u64, +) -> StdResult { + let mut validators = deps.querier.query_validators()?; + let bounds = (config_r(&deps.storage).load()?).validator_bounds; + + // filter down to viable candidates + if let Some(bounds) = bounds { + let mut candidates = vec![]; + for validator in validators { + if is_validator_inbounds(&validator, &bounds) { + candidates.push(validator); + } + } + validators = candidates; + } + + if validators.is_empty() { + return Err(StdError::GenericErr { + msg: "No validators within bounds".to_string(), + backtrace: None, + }); + } + // seed will likely be env.block.time + Ok(validators[(seed % validators.len() as u64) as usize].clone()) +} + +pub fn is_validator_inbounds(validator: &Validator, bounds: &ValidatorBounds) -> bool { + validator.commission <= bounds.max_commission && validator.commission >= bounds.min_commission +} diff --git a/contracts/scrt_staking/src/lib.rs b/contracts/scrt_staking/src/lib.rs new file mode 100644 index 000000000..84be1cef6 --- /dev/null +++ b/contracts/scrt_staking/src/lib.rs @@ -0,0 +1,49 @@ +pub mod contract; +pub mod handle; +pub mod query; +pub mod state; + +#[cfg(test)] +mod test; + +#[cfg(target_arch = "wasm32")] +mod wasm { + use super::contract; + use cosmwasm_std::{ + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, + }; + + #[no_mangle] + extern "C" fn init(env_ptr: u32, msg_ptr: u32) -> u32 { + do_init( + &contract::init::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn handle(env_ptr: u32, msg_ptr: u32) -> u32 { + do_handle( + &contract::handle::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn query(msg_ptr: u32) -> u32 { + do_query( + &contract::query::, + msg_ptr, + ) + } + + // Other C externs like cosmwasm_vm_version_1, allocate, deallocate are available + // automatically because we `use cosmwasm_std`. +} diff --git a/contracts/scrt_staking/src/query.rs b/contracts/scrt_staking/src/query.rs new file mode 100644 index 000000000..c8e2bebab --- /dev/null +++ b/contracts/scrt_staking/src/query.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{ + Api, + Delegation, + Extern, + FullDelegation, + HumanAddr, + Querier, + StdResult, + Storage, +}; +use shade_protocol::scrt_staking::QueryAnswer; + +use crate::state::{config_r, self_address_r}; + +pub fn config(deps: &Extern) -> StdResult { + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) +} + +pub fn delegations( + deps: &Extern, +) -> StdResult> { + let address = self_address_r(&deps.storage).load()?; + deps.querier.query_all_delegations(address) +} + +pub fn delegation( + deps: &Extern, + validator: HumanAddr, +) -> StdResult> { + let address = self_address_r(&deps.storage).load()?; + deps.querier.query_delegation(address, validator) +} diff --git a/contracts/scrt_staking/src/state.rs b/contracts/scrt_staking/src/state.rs new file mode 100644 index 000000000..d5f496c76 --- /dev/null +++ b/contracts/scrt_staking/src/state.rs @@ -0,0 +1,43 @@ +use cosmwasm_std::{HumanAddr, Storage}; +use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; +use shade_protocol::scrt_staking; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static SELF_ADDRESS: &[u8] = b"self_address"; +pub static VIEWING_KEY: &[u8] = b"viewing_key"; + +//pub static DELEGATIONS: &[u8] = b"delegations"; + +pub fn config_w(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn self_address_w(storage: &mut S) -> Singleton { + singleton(storage, SELF_ADDRESS) +} + +pub fn self_address_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, SELF_ADDRESS) +} + +pub fn viewing_key_w(storage: &mut S) -> Singleton { + singleton(storage, VIEWING_KEY) +} + +pub fn viewing_key_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, VIEWING_KEY) +} + +/* +pub fn delegations_r(storage: &S) -> ReadonlySingleton> { + singleton_read(storage, DELEGATIONS) +} + +pub fn delegations_w(storage: &mut S) -> Singleton> { + singleton(storage, DELEGATIONS) +} +*/ diff --git a/contracts/scrt_staking/src/test.rs b/contracts/scrt_staking/src/test.rs new file mode 100644 index 000000000..3e1406c89 --- /dev/null +++ b/contracts/scrt_staking/src/test.rs @@ -0,0 +1,46 @@ +/* +#[cfg(test)] +pub mod tests { + use cosmwasm_std::{ + testing::{ + mock_dependencies, mock_env, MockStorage, MockApi, MockQuerier + }, + HumanAddr, + coins, from_binary, StdError, Uint128, + Extern, + }; + use shade_protocol::{ + treasury::{ + QueryAnswer, InitMsg, HandleMsg, + QueryMsg, + }, + asset::Contract, + }; + + use crate::{ + contract::{ + init, handle, query, + }, + }; + + fn create_contract(address: &str, code_hash: &str) -> Contract { + let env = mock_env(address.to_string(), &[]); + return Contract{ + address: env.message.sender, + code_hash: code_hash.to_string() + } + } + + fn dummy_init(admin: String, viewing_key: String) -> Extern { + let mut deps = mock_dependencies(20, &[]); + let msg = InitMsg { + admin: Option::from(HumanAddr(admin.clone())), + viewing_key, + }; + let env = mock_env(admin, &coins(1000, "earth")); + let _res = init(&mut deps, env, msg).unwrap(); + + return deps + } +} +*/ diff --git a/contracts/staking/.cargo/config b/contracts/staking/.cargo/config new file mode 100644 index 000000000..882fe08f6 --- /dev/null +++ b/contracts/staking/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/staking/.circleci/config.yml b/contracts/staking/.circleci/config.yml new file mode 100644 index 000000000..127e1ae7d --- /dev/null +++ b/contracts/staking/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +jobs: + build: + docker: + - image: rust:1.43.1 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + - run: + name: Build + command: cargo wasm --locked + - run: + name: Unit tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Integration tests + command: cargo integration-test --locked + - run: + name: Format source code + command: cargo fmt + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in source code and schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + - target/wasm32-unknown-unknown/release/.fingerprint + - target/wasm32-unknown-unknown/release/build + - target/wasm32-unknown-unknown/release/deps + key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} diff --git a/contracts/staking/Cargo.toml b/contracts/staking/Cargo.toml new file mode 100644 index 000000000..65707cc80 --- /dev/null +++ b/contracts/staking/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "staking" +version = "0.1.0" +authors = ["Guy Garcia "] +edition = "2018" + +exclude = [ + # 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. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +debug-print = ["cosmwasm-std/debug-print"] + +[dependencies] +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } +shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +snafu = { version = "0.6.3" } +mockall = "0.10.2" +mockall_double = "0.2.0" +binary-heap-plus = { version = "0.4.1", features = ["serde"] } diff --git a/contracts/staking/Makefile b/contracts/staking/Makefile new file mode 100644 index 000000000..2493c22f4 --- /dev/null +++ b/contracts/staking/Makefile @@ -0,0 +1,68 @@ +.PHONY: check +check: + cargo check + +.PHONY: clippy +clippy: + cargo clippy + +PHONY: test +test: unit-test + +.PHONY: unit-test +unit-test: + cargo test + +# This is a local build with debug-prints activated. Debug prints only show up +# in the local development chain (see the `start-server` command below) +# and mainnet won't accept contracts built with the feature enabled. +.PHONY: build _build +build: _build compress-wasm +_build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --features="debug-print" + +# This is a build suitable for uploading to mainnet. +# Calls to `debug_print` get removed by the compiler. +.PHONY: build-mainnet _build-mainnet +build-mainnet: _build-mainnet compress-wasm +_build-mainnet: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +# like build-mainnet, but slower and more deterministic +.PHONY: build-mainnet-reproducible +build-mainnet-reproducible: + docker run --rm -v "$$(pwd)":/contract \ + --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/contract/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + enigmampc/secret-contract-optimizer:1.0.3 + +.PHONY: compress-wasm +compress-wasm: + cp ./target/wasm32-unknown-unknown/release/*.wasm ./contract.wasm + @## The following line is not necessary, may work only on linux (extra size optimization) + @# wasm-opt -Os ./contract.wasm -o ./contract.wasm + cat ./contract.wasm | gzip -9 > ./contract.wasm.gz + +.PHONY: schema +schema: + cargo run --example schema + +# Run local development chain with four funded accounts (named a, b, c, and d) +.PHONY: start-server +start-server: # CTRL+C to stop + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1317:1317 \ + -v $$(pwd):/root/code \ + --name secretdev enigmampc/secret-network-sw-dev:v1.0.4-3 + +# This relies on running `start-server` in another console +# You can run other commands on the secretcli inside the dev image +# by using `docker exec secretdev secretcli`. +.PHONY: store-contract-local +store-contract-local: + docker exec secretdev secretcli tx compute store -y --from a --gas 1000000 /root/code/contract.wasm.gz + +.PHONY: clean +clean: + cargo clean + -rm -f ./contract.wasm ./contract.wasm.gz diff --git a/contracts/staking/src/contract.rs b/contracts/staking/src/contract.rs new file mode 100644 index 000000000..240cd6444 --- /dev/null +++ b/contracts/staking/src/contract.rs @@ -0,0 +1,114 @@ +use crate::{ + handle::{ + try_claim_rewards, + try_claim_unbond, + try_set_viewing_key, + try_stake, + try_unbond, + try_update_config, + try_vote, + }, + query, + state::{config_w, stake_state_w, unbonding_w}, +}; +use binary_heap_plus::BinaryHeap; +use cosmwasm_std::{ + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdResult, + Storage, + Uint128, +}; +use secret_toolkit::snip20::register_receive_msg; +use shade_protocol::{ + asset::Contract, + staking::{stake::Stake, Config, HandleMsg, InitMsg, QueryMsg}, +}; + +pub fn init( + deps: &mut Extern, + env: Env, + msg: InitMsg, +) -> StdResult { + let state = Config { + admin: match msg.admin { + None => Contract { + address: env.message.sender.clone(), + code_hash: "".to_string(), + }, + Some(admin) => admin, + }, + unbond_time: msg.unbond_time, + staked_token: msg.staked_token, + }; + + config_w(&mut deps.storage).save(&state)?; + + // Register staked_token + let cosmos_msg = register_receive_msg( + env.contract_code_hash, + None, + 256, + state.staked_token.code_hash.clone(), + state.staked_token.address, + )?; + + // Initialize binary heap + let unbonding_heap = BinaryHeap::new_min(); + unbonding_w(&mut deps.storage).save(&unbonding_heap)?; + + // Initialize stake state + stake_state_w(&mut deps.storage).save(&Stake { + total_shares: Uint128::zero(), + total_tokens: Uint128::zero(), + })?; + + Ok(InitResponse { + messages: vec![cosmos_msg], + log: vec![], + }) +} + +pub fn handle( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + match msg { + HandleMsg::UpdateConfig { admin, unbond_time } => { + try_update_config(deps, &env, admin, unbond_time) + } + HandleMsg::Receive { + sender, + from, + amount, + } => try_stake(deps, &env, sender, from, amount), + HandleMsg::Unbond { amount } => try_unbond(deps, &env, amount), + HandleMsg::Vote { proposal_id, votes } => try_vote(deps, &env, proposal_id, votes), + HandleMsg::ClaimUnbond {} => try_claim_unbond(deps, &env), + HandleMsg::ClaimRewards {} => try_claim_rewards(deps, &env), + HandleMsg::SetViewingKey { key } => try_set_viewing_key(deps, &env, key), + } +} + +pub fn query( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&query::config(deps)?), + QueryMsg::TotalStaked {} => to_binary(&query::total_staked(deps)?), + QueryMsg::TotalUnbonding { start, end } => { + to_binary(&query::total_unbonding(deps, start, end)?) + } + QueryMsg::UserStake { address, key, time } => { + to_binary(&query::user_stake(deps, address, key, time)?) + } + } +} diff --git a/contracts/staking/src/handle.rs b/contracts/staking/src/handle.rs new file mode 100644 index 000000000..2820736a3 --- /dev/null +++ b/contracts/staking/src/handle.rs @@ -0,0 +1,375 @@ +use crate::state::{ + config_r, + config_w, + stake_state_r, + stake_state_w, + staker_r, + staker_w, + unbonding_w, + user_unbonding_w, + viewking_key_w, +}; +use binary_heap_plus::BinaryHeap; +use cosmwasm_std::{ + to_binary, + Api, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, + Uint128, +}; +use secret_toolkit::{snip20::send_msg, utils::HandleCallback}; +use shade_protocol::{ + asset::Contract, + generic_response::ResponseStatus::Success, + governance::vote::{UserVote, Vote, VoteTally}, + staking::{ + stake::{Stake, Unbonding, UserStake}, + HandleAnswer, + }, +}; + +pub(crate) fn calculate_shares(tokens: Uint128, state: &Stake) -> Uint128 { + if state.total_shares.is_zero() && state.total_tokens.is_zero() { + tokens + } else { + tokens.multiply_ratio(state.total_shares, state.total_tokens) + } +} + +pub(crate) fn calculate_tokens(shares: Uint128, state: &Stake) -> Uint128 { + if state.total_shares.is_zero() && state.total_tokens.is_zero() { + shares + } else { + shares.multiply_ratio(state.total_tokens, state.total_shares) + } +} + +pub(crate) fn calculate_rewards(user: &UserStake, state: &Stake) -> Uint128 { + (calculate_tokens(user.shares, state) - user.tokens_staked).unwrap() +} + +pub fn try_update_config( + deps: &mut Extern, + env: &Env, + admin: Option, + unbond_time: Option, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + // Check if admin + if env.message.sender != config.admin.address { + return Err(StdError::Unauthorized { backtrace: None }); + } + + config_w(&mut deps.storage).update(|mut config| { + if let Some(admin) = admin { + config.admin = admin; + } + if let Some(unbond_time) = unbond_time { + config.unbond_time = unbond_time; + } + Ok(config) + })?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateUnbondTime { + status: Success, + })?), + }) +} + +pub fn try_stake( + deps: &mut Extern, + env: &Env, + sender: HumanAddr, + _from: HumanAddr, + amount: Uint128, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + // Check if staking token + if env.message.sender != config.staked_token.address { + return Err(StdError::Unauthorized { backtrace: None }); + } + + let mut state = stake_state_r(&deps.storage).load()?; + + // Either create a new account or add stake + staker_w(&mut deps.storage).update(sender.as_str().as_bytes(), |user_state| { + // Calculate shares proportional to stake amount + let shares = calculate_shares(amount, &state); + + let new_state = match user_state { + None => UserStake { + shares, + tokens_staked: amount, + }, + Some(mut user_state) => { + user_state.tokens_staked += amount; + user_state.shares += shares; + user_state + } + }; + + state.total_shares += shares; + state.total_tokens += amount; + + Ok(new_state) + })?; + + // Update total stake + stake_state_w(&mut deps.storage).save(&state)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Stake { status: Success })?), + }) +} + +pub fn try_unbond( + deps: &mut Extern, + env: &Env, + amount: Uint128, +) -> StdResult { + let sender = env.message.sender.clone(); + + let mut state = stake_state_r(&deps.storage).load()?; + + // Check if user has >= amount + staker_w(&mut deps.storage).update(sender.to_string().as_bytes(), |user_state| { + let shares = calculate_shares(amount, &state); + + let new_state = match user_state { + None => { + return Err(StdError::GenericErr { + msg: "Not enough staked".to_string(), + backtrace: None, + }); + } + Some(user_state) => { + if user_state.tokens_staked >= amount { + UserStake { + shares: (user_state.shares - shares)?, + tokens_staked: (user_state.tokens_staked - amount)?, + } + } else { + return Err(StdError::GenericErr { + msg: "Not enough staked".to_string(), + backtrace: None, + }); + } + } + }; + + // Theres no pretty way of doing this + state.total_shares = (state.total_shares - shares)?; + state.total_tokens = (state.total_tokens - amount)?; + + Ok(new_state) + })?; + + let config = config_r(&deps.storage).load()?; + let unbonding = Unbonding { + amount, + unbond_time: env.block.time + config.unbond_time, + }; + + unbonding_w(&mut deps.storage).update(|mut unbonding_queue| { + unbonding_queue.push(unbonding.clone()); + Ok(unbonding_queue) + })?; + + user_unbonding_w(&mut deps.storage).update( + env.message.sender.to_string().as_bytes(), + |queue| { + let mut unbonding_queue = match queue { + None => BinaryHeap::new_min(), + Some(queue) => queue, + }; + + unbonding_queue.push(unbonding); + + Ok(unbonding_queue) + }, + )?; + + stake_state_w(&mut deps.storage).save(&state)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Unbond { status: Success })?), + }) +} + +pub fn stake_weight(stake: Uint128, weight: u8) -> Uint128 { + stake.multiply_ratio(weight, 100u128) +} + +pub fn try_vote( + deps: &mut Extern, + env: &Env, + proposal_id: Uint128, + votes: Vec, +) -> StdResult { + let user_state = staker_r(&deps.storage).load(env.message.sender.to_string().as_bytes())?; + // check that percentage is <= 100 and calculate distribution + let mut total_votes = VoteTally { + yes: Uint128(0), + no: Uint128(0), + abstain: Uint128(0), + }; + + let mut count = 0; + + for vote in votes { + match vote.vote { + Vote::Yes => { + total_votes.yes += stake_weight(user_state.tokens_staked, vote.weight); + } + Vote::No => { + total_votes.no += stake_weight(user_state.tokens_staked, vote.weight); + } + Vote::Abstain => { + total_votes.abstain += stake_weight(user_state.tokens_staked, vote.weight); + } + }; + count += vote.weight; + } + + if count > 100 { + return Err(StdError::GenericErr { + msg: "Total weight must be 100 or less".to_string(), + backtrace: None, + }); + } + + // Admin is governance, send to governance + let config = config_r(&deps.storage).load()?; + let messages = vec![ + shade_protocol::governance::HandleMsg::MakeVote { + voter: env.message.sender.clone(), + proposal_id, + votes: total_votes, + } + .to_cosmos_msg(config.admin.code_hash, config.admin.address, None)?, + ]; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::Vote { status: Success })?), + }) +} + +pub fn try_claim_unbond( + deps: &mut Extern, + env: &Env, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + let mut total = Uint128::zero(); + + let mut messages = vec![]; + + user_unbonding_w(&mut deps.storage).update( + env.message.sender.clone().to_string().as_bytes(), + |queue| { + let mut new_queue = queue.ok_or_else(|| StdError::not_found("user"))?; + + while let Some(unbonding) = new_queue.peek() { + if env.block.time < unbonding.unbond_time { + break; + } + + total += unbonding.amount; + new_queue.pop(); + } + + messages.push(send_msg( + env.message.sender.clone(), + total, + None, + None, + None, + 1, + config.staked_token.code_hash.clone(), + config.staked_token.address.clone(), + )?); + + Ok(new_queue) + }, + )?; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::ClaimUnbond { status: Success })?), + }) +} + +pub fn try_claim_rewards( + deps: &mut Extern, + env: &Env, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + let mut state = stake_state_r(&deps.storage).load()?; + let mut messages = vec![]; + + staker_w(&mut deps.storage).update( + env.message.sender.to_string().as_bytes(), + |user_state| { + let mut user = user_state.ok_or_else(|| StdError::NotFound { + kind: "user".to_string(), + backtrace: None, + })?; + + let rewards = calculate_rewards(&user, &state); + let shares = calculate_shares(rewards, &state); + user.shares = (user.shares - shares)?; + state.total_shares = (state.total_shares - shares)?; + state.total_tokens = (state.total_tokens - rewards)?; + + messages.push(send_msg( + env.message.sender.clone(), + rewards, + None, + None, + None, + 1, + config.staked_token.code_hash.clone(), + config.staked_token.address.clone(), + )?); + + Ok(user) + }, + )?; + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::ClaimRewards { status: Success })?), + }) +} + +pub fn try_set_viewing_key( + deps: &mut Extern, + env: &Env, + key: String, +) -> StdResult { + viewking_key_w(&mut deps.storage).save(env.message.sender.to_string().as_bytes(), &key)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::SetViewingKey { status: Success })?), + }) +} diff --git a/contracts/staking/src/lib.rs b/contracts/staking/src/lib.rs new file mode 100644 index 000000000..84be1cef6 --- /dev/null +++ b/contracts/staking/src/lib.rs @@ -0,0 +1,49 @@ +pub mod contract; +pub mod handle; +pub mod query; +pub mod state; + +#[cfg(test)] +mod test; + +#[cfg(target_arch = "wasm32")] +mod wasm { + use super::contract; + use cosmwasm_std::{ + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, + }; + + #[no_mangle] + extern "C" fn init(env_ptr: u32, msg_ptr: u32) -> u32 { + do_init( + &contract::init::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn handle(env_ptr: u32, msg_ptr: u32) -> u32 { + do_handle( + &contract::handle::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn query(msg_ptr: u32) -> u32 { + do_query( + &contract::query::, + msg_ptr, + ) + } + + // Other C externs like cosmwasm_vm_version_1, allocate, deallocate are available + // automatically because we `use cosmwasm_std`. +} diff --git a/contracts/staking/src/query.rs b/contracts/staking/src/query.rs new file mode 100644 index 000000000..fd5bc4859 --- /dev/null +++ b/contracts/staking/src/query.rs @@ -0,0 +1,79 @@ +use crate::{ + handle::calculate_rewards, + state::{config_r, stake_state_r, staker_r, unbonding_r, user_unbonding_r, viewking_key_r}, +}; +use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage, Uint128}; +use shade_protocol::staking::QueryAnswer; + +pub fn config(deps: &Extern) -> StdResult { + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) +} + +pub fn total_staked( + deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::TotalStaked { + total: stake_state_r(&deps.storage).load()?.total_tokens, + }) +} + +pub fn total_unbonding( + deps: &Extern, + start_limit: Option, + end_limit: Option, +) -> StdResult { + let mut total = Uint128::zero(); + let mut queue = unbonding_r(&deps.storage).load()?; + + let start = start_limit.unwrap_or(0u64); + + let end = end_limit.unwrap_or(u64::MAX); + + while let Some(item) = queue.pop() { + if start <= item.unbond_time && item.unbond_time <= end { + total += item.amount; + } + } + + Ok(QueryAnswer::TotalUnbonding { total }) +} + +pub fn user_stake( + deps: &Extern, + address: HumanAddr, + key: String, + time: u64, +) -> StdResult { + if viewking_key_r(&deps.storage).load(address.to_string().as_bytes())? != key { + return Err(StdError::Unauthorized { backtrace: None }); + } + + let state = stake_state_r(&deps.storage).load()?; + let user_state = staker_r(&deps.storage).load(address.to_string().as_bytes())?; + + let mut unbonding = Uint128::zero(); + let mut unbonded = Uint128::zero(); + + let queue = user_unbonding_r(&deps.storage).may_load(address.to_string().as_bytes())?; + + if let Some(mut queue) = queue { + while !queue.is_empty() { + let item = queue.pop().unwrap(); + + if item.unbond_time > time { + unbonding += item.amount; + } else { + unbonded += item.amount; + } + } + } + + Ok(QueryAnswer::UserStake { + staked: user_state.tokens_staked, + pending_rewards: calculate_rewards(&user_state, &state), + unbonding, + unbonded, + }) +} diff --git a/contracts/staking/src/state.rs b/contracts/staking/src/state.rs new file mode 100644 index 000000000..deaad8543 --- /dev/null +++ b/contracts/staking/src/state.rs @@ -0,0 +1,81 @@ +use cosmwasm_std::Storage; +use cosmwasm_storage::{ + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, +}; + +use binary_heap_plus::{BinaryHeap, MinComparator}; +use shade_protocol::staking::{ + stake::{Stake, Unbonding, UserStake}, + Config, +}; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static STAKE_STATE_KEY: &[u8] = b"stake_state"; +pub static STAKER_KEY: &[u8] = b"staker"; +pub static UNBONDING_KEY: &[u8] = b"unbonding"; +pub static USER_UNBONDING_KEY: &[u8] = b"user_unbonding"; +pub static VIEWKING_KEY: &[u8] = b"viewing_key"; + +pub fn config_w(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn stake_state_w(storage: &mut S) -> Singleton { + singleton(storage, STAKE_STATE_KEY) +} + +pub fn stake_state_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, STAKE_STATE_KEY) +} + +pub fn staker_r(storage: &S) -> ReadonlyBucket { + bucket_read(STAKER_KEY, storage) +} + +pub fn staker_w(storage: &mut S) -> Bucket { + bucket(STAKER_KEY, storage) +} + +// Ideally these queues will be removed +pub fn unbonding_w( + storage: &mut S, +) -> Singleton> { + singleton(storage, UNBONDING_KEY) +} + +pub fn unbonding_r( + storage: &S, +) -> ReadonlySingleton> { + singleton_read(storage, UNBONDING_KEY) +} + +pub fn user_unbonding_r( + storage: &S, +) -> ReadonlyBucket> { + bucket_read(USER_UNBONDING_KEY, storage) +} + +pub fn user_unbonding_w( + storage: &mut S, +) -> Bucket> { + bucket(USER_UNBONDING_KEY, storage) +} + +pub fn viewking_key_r(storage: &S) -> ReadonlyBucket { + bucket_read(VIEWKING_KEY, storage) +} + +pub fn viewking_key_w(storage: &mut S) -> Bucket { + bucket(VIEWKING_KEY, storage) +} diff --git a/contracts/staking/src/test.rs b/contracts/staking/src/test.rs new file mode 100644 index 000000000..c2492e16b --- /dev/null +++ b/contracts/staking/src/test.rs @@ -0,0 +1,174 @@ +#[cfg(test)] +pub mod tests { + use crate::handle::{calculate_shares, calculate_tokens, stake_weight}; + use binary_heap_plus::{BinaryHeap, MinComparator}; + use cosmwasm_std::Uint128; + use shade_protocol::staking::stake::{Stake, Unbonding, UserStake}; + + #[test] + fn test_weight_calculation() { + let stake = Uint128(1000000); + + assert_eq!(Uint128(500000), stake_weight(stake, 50)); + assert_eq!(Uint128(250000), stake_weight(stake, 25)); + } + + #[test] + fn binary_heap_order() { + let mut unbonding_heap: BinaryHeap = BinaryHeap::new_min(); + + // Add the three values in a non order fashion + let val1 = Unbonding { + amount: Default::default(), + unbond_time: 0, + }; + let val2 = Unbonding { + amount: Default::default(), + unbond_time: 1, + }; + let val3 = Unbonding { + amount: Default::default(), + unbond_time: 2, + }; + + unbonding_heap.push(val2); + unbonding_heap.push(val1); + unbonding_heap.push(val3); + + assert_eq!(0, unbonding_heap.pop().unwrap().unbond_time); + assert_eq!(1, unbonding_heap.pop().unwrap().unbond_time); + assert_eq!(2, unbonding_heap.pop().unwrap().unbond_time); + } + + fn init_user() -> UserStake { + UserStake { + shares: Uint128::zero(), + tokens_staked: Uint128::zero(), + } + } + + fn stake(state: &mut Stake, user: &mut UserStake, amount: Uint128) -> Uint128 { + let shares = calculate_shares(amount, state); + state.total_tokens += amount; + state.total_shares += shares; + user.tokens_staked += amount; + user.shares += shares; + + shares + } + + fn unbond(state: &mut Stake, user: &mut UserStake, amount: Uint128) -> Uint128 { + let shares = calculate_shares(amount, state); + state.total_tokens = (state.total_tokens - amount).unwrap(); + state.total_shares = (state.total_shares - shares).unwrap(); + user.tokens_staked = (user.tokens_staked - amount).unwrap(); + user.shares = (user.shares - shares).unwrap(); + + shares + } + + #[test] + fn standard_staking() { + let mut state = Stake { + total_shares: Uint128::zero(), + total_tokens: Uint128::zero(), + }; + + // User 1 stakes 100 + let mut u1 = init_user(); + let u1_stake = Uint128(100); + stake(&mut state, &mut u1, u1_stake); + + assert_eq!(u1_stake, calculate_tokens(u1.shares, &state)); + + // User 2 stakes 50 + let mut u2 = init_user(); + let u2_stake = Uint128(50); + stake(&mut state, &mut u2, u2_stake); + + assert_eq!(u1_stake, calculate_tokens(u1.shares, &state)); + assert_eq!(u2_stake, calculate_tokens(u2.shares, &state)); + + // User 3 stakes 35 + let mut u3 = init_user(); + let u3_stake = Uint128(35); + stake(&mut state, &mut u3, u3_stake); + + assert_eq!(u1_stake, calculate_tokens(u1.shares, &state)); + assert_eq!(u2_stake, calculate_tokens(u2.shares, &state)); + assert_eq!(u3_stake, calculate_tokens(u3.shares, &state)); + } + + #[test] + fn unbonding() { + let mut state = Stake { + total_shares: Uint128::zero(), + total_tokens: Uint128::zero(), + }; + + // User 1 stakes 100 + let mut u1 = init_user(); + let u1_stake = Uint128(100); + stake(&mut state, &mut u1, u1_stake); + + // User 2 stakes 50 + let mut u2 = init_user(); + let u2_stake = Uint128(50); + stake(&mut state, &mut u2, u2_stake); + + // User 3 stakes 35 + let mut u3 = init_user(); + let u3_stake = Uint128(35); + stake(&mut state, &mut u3, u3_stake); + + // User 2 unbonds 25 + let u2_unbond = Uint128(25); + unbond(&mut state, &mut u2, u2_unbond); + + assert_eq!(u1_stake, calculate_tokens(u1.shares, &state)); + assert_eq!( + (u2_stake - u2_unbond).unwrap(), + calculate_tokens(u2.shares, &state) + ); + assert_eq!(u3_stake, calculate_tokens(u3.shares, &state)); + } + + #[test] + fn rewards_distribution() { + let mut state = Stake { + total_shares: Uint128::zero(), + total_tokens: Uint128::zero(), + }; + + // User 1 stakes 100 + let mut u1 = init_user(); + let u1_stake = Uint128(100); + stake(&mut state, &mut u1, u1_stake); + + // User 2 stakes 50 + let mut u2 = init_user(); + let u2_stake = Uint128(50); + stake(&mut state, &mut u2, u2_stake); + + // User 3 stakes 50 + let mut u3 = init_user(); + let u3_stake = Uint128(50); + stake(&mut state, &mut u3, u3_stake); + + // Add a 200 reward, (should double user amounts) + state.total_tokens += Uint128(200); + + assert_eq!( + u1_stake.multiply_ratio(Uint128(2), Uint128(1)), + calculate_tokens(u1.shares, &state) + ); + assert_eq!( + u2_stake.multiply_ratio(Uint128(2), Uint128(1)), + calculate_tokens(u2.shares, &state) + ); + assert_eq!( + u3_stake.multiply_ratio(Uint128(2), Uint128(1)), + calculate_tokens(u3.shares, &state) + ); + } +} diff --git a/contracts/treasury/Cargo.toml b/contracts/treasury/Cargo.toml index a5804f796..aaceef244 100644 --- a/contracts/treasury/Cargo.toml +++ b/contracts/treasury/Cargo.toml @@ -15,17 +15,6 @@ exclude = [ [lib] crate-type = ["cdylib", "rlib"] -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] default = [] # for quicker tests, cargo test --lib @@ -34,10 +23,10 @@ backtraces = ["cosmwasm-std/backtraces"] debug-print = ["cosmwasm-std/debug-print"] [dependencies] -cosmwasm-schema = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-std = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-storage = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -secret-toolkit = { git = "https://github.com/enigmampc/secret-toolkit", branch = "debug-print"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/treasury/rustfmt.toml b/contracts/treasury/rustfmt.toml deleted file mode 100644 index 11a85e6a9..000000000 --- a/contracts/treasury/rustfmt.toml +++ /dev/null @@ -1,15 +0,0 @@ -# stable -newline_style = "unix" -hard_tabs = false -tab_spaces = 4 - -# unstable... should we require `rustup run nightly cargo fmt` ? -# or just update the style guide when they are stable? -#fn_single_line = true -#format_code_in_doc_comments = true -#overflow_delimited_expr = true -#reorder_impl_items = true -#struct_field_align_threshold = 20 -#struct_lit_single_line = true -#report_todo = "Always" - diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index 6e1e62b4c..69cc0cb58 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -1,37 +1,34 @@ use cosmwasm_std::{ - debug_print, to_binary, Api, Binary, - Env, Extern, HandleResponse, InitResponse, - Querier, StdResult, Storage, + debug_print, + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + InitResponse, + Querier, + StdResult, + Storage, }; -use shade_protocol::{ - treasury::{ - InitMsg, TreasuryConfig, - HandleMsg, - QueryMsg, - }, -}; +use shade_protocol::treasury::{Config, HandleMsg, InitMsg, QueryMsg}; use crate::{ - state::{ - viewing_key_w, - config_w, - self_address_w, - }, - handle, query, + handle, + query, + state::{config_w, self_address_w, viewing_key_w}, }; - pub fn init( deps: &mut Extern, env: Env, msg: InitMsg, ) -> StdResult { - - let state = TreasuryConfig { + let state = Config { owner: match msg.admin { - None => { env.message.sender.clone() } - Some(admin) => { admin } + None => env.message.sender.clone(), + Some(admin) => admin, }, }; @@ -43,7 +40,7 @@ pub fn init( Ok(InitResponse { messages: vec![], - log: vec![] + log: vec![], }) } @@ -60,12 +57,12 @@ pub fn handle( msg, .. } => handle::receive(deps, env, sender, from, amount, msg), - HandleMsg::UpdateConfig { - owner, - } => handle::try_update_config(deps, env, owner), + HandleMsg::UpdateConfig { owner } => handle::try_update_config(deps, env, owner), HandleMsg::RegisterAsset { contract, - } => handle::try_register_asset(deps, &env, &contract), + allocations, + } => handle::try_register_asset(deps, &env, &contract, allocations), + HandleMsg::Rebalance {} => handle::rebalance(deps, &env), } } @@ -76,5 +73,6 @@ pub fn query( match msg { QueryMsg::GetConfig {} => to_binary(&query::config(deps)?), QueryMsg::GetBalance { contract } => to_binary(&query::balance(deps, contract)?), + QueryMsg::CanRebalance {} => to_binary(&query::can_rebalance(deps)?), } } diff --git a/contracts/treasury/src/handle.rs b/contracts/treasury/src/handle.rs index 965133c8b..fb3926b8a 100644 --- a/contracts/treasury/src/handle.rs +++ b/contracts/treasury/src/handle.rs @@ -1,31 +1,27 @@ use cosmwasm_std::{ - debug_print, to_binary, Api, Binary, - Env, Extern, HandleResponse, - Querier, StdError, StdResult, Storage, - CosmosMsg, HumanAddr, Uint128 -}; -use secret_toolkit::{ - snip20::{ - token_info_query, - register_receive_msg, - set_viewing_key_msg, - }, + debug_print, + to_binary, + Api, + Binary, + Env, + Extern, + HandleResponse, + HumanAddr, + Querier, + StdError, + StdResult, + Storage, + Uint128, }; +use secret_toolkit::snip20::{register_receive_msg, set_viewing_key_msg, token_info_query}; use shade_protocol::{ - treasury::{ - HandleAnswer, - Snip20Asset - }, asset::Contract, generic_response::ResponseStatus, + treasury::{Allocation, Asset, HandleAnswer}, }; -use crate::state::{ - config_w, config_r, - assets_r, assets_w, - viewing_key_r, -}; +use crate::state::{assets_r, assets_w, config_r, config_w, viewing_key_r}; pub fn receive( deps: &mut Extern, @@ -35,18 +31,17 @@ pub fn receive( amount: Uint128, _msg: Option, ) -> StdResult { - let assets = assets_r(&deps.storage); - let asset: Snip20Asset = assets.load(env.message.sender.to_string().as_bytes())?; + let asset: Asset = assets.load(env.message.sender.to_string().as_bytes())?; debug_print!("Treasured {} u{}", amount, asset.token_info.symbol); Ok(HandleResponse { messages: vec![], log: vec![], - data: Some( to_binary( &HandleAnswer::Receive { + data: Some(to_binary(&HandleAnswer::Receive { status: ResponseStatus::Success, - } )? ), + })?), }) } @@ -55,7 +50,6 @@ pub fn try_update_config( env: Env, owner: Option, ) -> StdResult { - let config = config_r(&deps.storage).load()?; if env.message.sender != config.owner { return Err(StdError::Unauthorized { backtrace: None }); @@ -73,8 +67,9 @@ pub fn try_update_config( Ok(HandleResponse { messages: vec![], log: vec![], - data: Some( to_binary( &HandleAnswer::UpdateConfig { - status: ResponseStatus::Success } )? ) + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), }) } @@ -82,57 +77,64 @@ pub fn try_register_asset( deps: &mut Extern, env: &Env, contract: &Contract, + allocations: Option>, ) -> StdResult { - let config = config_r(&deps.storage).load()?; if env.message.sender != config.owner { return Err(StdError::Unauthorized { backtrace: None }); } let mut messages = vec![]; - let token_info = token_info_query(&deps.querier, 1, - contract.code_hash.clone(), - contract.address.clone())?; + let token_info = token_info_query( + &deps.querier, + 1, + contract.code_hash.clone(), + contract.address.clone(), + )?; - assets_w(&mut deps.storage).save(contract.address.to_string().as_bytes(), &Snip20Asset { + assets_w(&mut deps.storage).save(contract.address.to_string().as_bytes(), &Asset { contract: contract.clone(), token_info, + allocations, })?; // Register contract in asset - messages.push(register_receive(&env, &contract)?); + messages.push(register_receive_msg( + env.contract_code_hash.clone(), + None, + 256, + contract.code_hash.clone(), + contract.address.clone(), + )?); // Set viewing key messages.push(set_viewing_key_msg( - viewing_key_r(&deps.storage).load()?, - None, - 1, - contract.code_hash.clone(), - contract.address.clone())?); - + viewing_key_r(&deps.storage).load()?, + None, + 1, + contract.code_hash.clone(), + contract.address.clone(), + )?); Ok(HandleResponse { messages, log: vec![], - data: Some( to_binary( - &HandleAnswer::RegisterAsset { - status: ResponseStatus::Success } - )? - ) + data: Some(to_binary(&HandleAnswer::RegisterAsset { + status: ResponseStatus::Success, + })?), }) } -pub fn register_receive ( - env: &Env, - contract: &Contract, -) -> StdResult { - let cosmos_msg = register_receive_msg( - env.contract_code_hash.clone(), - None, - 256, - contract.code_hash.clone(), - contract.address.clone(), - ); - - cosmos_msg +pub fn rebalance( + _deps: &mut Extern, + _env: &Env, +) -> StdResult { + let messages = vec![]; + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::Rebalance { + status: ResponseStatus::Success, + })?), + }) } diff --git a/contracts/treasury/src/lib.rs b/contracts/treasury/src/lib.rs index 14d4d23b1..84be1cef6 100644 --- a/contracts/treasury/src/lib.rs +++ b/contracts/treasury/src/lib.rs @@ -1,7 +1,7 @@ pub mod contract; -pub mod state; pub mod handle; pub mod query; +pub mod state; #[cfg(test)] mod test; @@ -10,7 +10,12 @@ mod test; mod wasm { use super::contract; use cosmwasm_std::{ - do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + do_handle, + do_init, + do_query, + ExternalApi, + ExternalQuerier, + ExternalStorage, }; #[no_mangle] diff --git a/contracts/treasury/src/query.rs b/contracts/treasury/src/query.rs index 15484c842..bf949d664 100644 --- a/contracts/treasury/src/query.rs +++ b/contracts/treasury/src/query.rs @@ -1,52 +1,33 @@ -use cosmwasm_std::{ - Api, Extern, Querier, Storage, - StdResult, StdError, HumanAddr, -}; +use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage}; use secret_toolkit::snip20; -use shade_protocol::{ - treasury::{ - QueryAnswer - }, -}; +use shade_protocol::treasury::QueryAnswer; -use crate::state::{ - config_r, - viewing_key_r, - self_address_r, - assets_r, -}; +use crate::state::{assets_r, config_r, self_address_r, viewing_key_r}; -pub fn config( - deps: &Extern -) -> StdResult { - - Ok(QueryAnswer::Config { config: config_r(&deps.storage).load()? }) +pub fn config(deps: &Extern) -> StdResult { + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) } pub fn balance( deps: &Extern, contract: HumanAddr, ) -> StdResult { - //TODO: restrict to admin - match assets_r(&deps.storage).may_load(&contract.to_string().as_bytes())? { - Some(a) => { - return Ok(snip20::QueryMsg::Balance { - address: self_address_r(&deps.storage).load()?, - key: viewing_key_r(&deps.storage).load()?, - }.query( - &deps.querier, - 1, - a.contract.code_hash, - contract.clone(), - )?) - } - None => { - return Err(StdError::NotFound { - kind: contract.to_string(), - backtrace: None }) + return match assets_r(&deps.storage).may_load(contract.to_string().as_bytes())? { + Some(a) => Ok(snip20::QueryMsg::Balance { + address: self_address_r(&deps.storage).load()?, + key: viewing_key_r(&deps.storage).load()?, } + .query(&deps.querier, 1, a.contract.code_hash, contract)?), + None => Err(StdError::not_found(contract.to_string())), }; +} +pub fn can_rebalance( + _deps: &Extern, +) -> StdResult { + Ok(QueryAnswer::CanRebalance { possible: false }) } diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index 96b0003e5..ab8302269 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -1,8 +1,15 @@ -use cosmwasm_std::{Storage, HumanAddr}; -use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton, bucket, Bucket, bucket_read, ReadonlyBucket}; -use shade_protocol::{ - treasury::{TreasuryConfig, Snip20Asset}, +use cosmwasm_std::{HumanAddr, Storage}; +use cosmwasm_storage::{ + bucket, + bucket_read, + singleton, + singleton_read, + Bucket, + ReadonlyBucket, + ReadonlySingleton, + Singleton, }; +use shade_protocol::treasury; pub static CONFIG_KEY: &[u8] = b"config"; pub static ASSET_KEY: &[u8] = b"assets"; @@ -10,11 +17,11 @@ pub static ASSET_LIST_KEY: &[u8] = b"asset_list"; pub static VIEWING_KEY: &[u8] = b"viewing_key"; pub static SELF_ADDRESS: &[u8] = b"self_address"; -pub fn config_w(storage: &mut S) -> Singleton { +pub fn config_w(storage: &mut S) -> Singleton { singleton(storage, CONFIG_KEY) } -pub fn config_r(storage: &S) -> ReadonlySingleton { +pub fn config_r(storage: &S) -> ReadonlySingleton { singleton_read(storage, CONFIG_KEY) } @@ -26,11 +33,11 @@ pub fn asset_list_read(storage: &S) -> ReadonlySingleton(storage: & S) -> ReadonlyBucket { +pub fn assets_r(storage: &S) -> ReadonlyBucket { bucket_read(ASSET_KEY, storage) } -pub fn assets_w(storage: &mut S) -> Bucket { +pub fn assets_w(storage: &mut S) -> Bucket { bucket(ASSET_KEY, storage) } diff --git a/contracts/treasury/src/test.rs b/contracts/treasury/src/test.rs index dc407620e..879d17eb4 100644 --- a/contracts/treasury/src/test.rs +++ b/contracts/treasury/src/test.rs @@ -1,25 +1,18 @@ #[cfg(test)] pub mod tests { + /* use cosmwasm_std::{ testing::{ mock_dependencies, mock_env, MockStorage, MockApi, MockQuerier }, - HumanAddr, - coins, from_binary, StdError, Uint128, - Extern, + HumanAddr, coins, Extern, }; use shade_protocol::{ - treasury::{ - QueryAnswer, InitMsg, HandleMsg, - QueryMsg, - }, - asset::Contract, + treasury::InitMsg, }; use crate::{ - contract::{ - init, handle, query, - }, + contract::init, }; fn create_contract(address: &str, code_hash: &str) -> Contract { @@ -41,4 +34,5 @@ pub mod tests { return deps } + */ } diff --git a/deploy_network.py b/deploy_network.py deleted file mode 100755 index dbdcce70c..000000000 --- a/deploy_network.py +++ /dev/null @@ -1,379 +0,0 @@ -#!/usr/bin/env python3 -import random -import click -import json -from sys import exit -from collections import defaultdict - - -from contractlib.contractlib import PreInstantiatedContract -from contractlib.contractlib import Contract -from contractlib.secretlib import secretlib -from contractlib.snip20lib import SNIP20 -from contractlib.micro_mintlib import MicroMint -from contractlib.oraclelib import Oracle -from contractlib.treasurylib import Treasury -from contractlib.utils import gen_label - -@click.command() -@click.option('--no_prime', is_flag=True) - - -def deploy_network(no_prime: bool): - if no_prime: - print('Deploying without priming') - else: - print('Will deploy then prime the network with funds, pass --no_prime to skip') - - # Burns for core_assets assets - entry_assets = [ - ('secretSCRT', 'SSCRT'), - ] - - core_assets = [ - # Core - ('Shade', 'SHD'), - ('Silk', 'SILK'), - # Synthetic assets stablecoin - #('Synthesis', 'SYN'), - ] - - # burn core assets for these, and back - synthetic_assets = [ - # Metals - ('Synthetic Gold', 'XAU'), - ('Synthetic Silver', 'XAG'), - - # Fiat - #('Synthetic Euro', 'EUR'), - #('Synthetic Japanese yen', 'JPY'), - #('Synthetic Yuan', 'CNY'), - - # Crypto - #('Synthetic Ethereum', 'ETH'), - #('Synthetic Bitcoin', 'BTC'), - #('Synthetic secret', 'SCRT'), - #('Synthetic Dogecoin', 'DOGE'), - #('Synthetic Uniswap', 'UNI'), - #('Synthetic Stellar', 'XLM'), - #('Synthetic PancakeSwap', 'CAKE'), - #('Synthetic Band Protocol', 'BAND'), - #('Synthetic Terra', 'LUNA'), - #('Synthetic Cosmos', 'ATOM'), - - # TODO: Oracle: add these sources - # Stocks - # ('Synthetic Tesla', 'TSLA'), - # ('Synthetic Apple', 'AAPL'), - # ('Synthetic Google', 'GOOG'), - ] - - TESTNET_BAND = { - "address": "secret1p0jtg47hhwuwgp4cjpc46m7qq6vyjhdsvy2nph", - "code_hash": "77c854ea110315d5103a42b88d3e7b296ca245d8b095e668c69997b265a75ac5" - } - - TESTNET_SSCRT = { - 'address': 'secret1s7c6xp9wltthk5r6mmavql4xld5me3g37guhsx', - 'code_hash': 'cd400fb73f5c99edbc6aab22c2593332b8c9f2ea806bf9b42e3a523f3ad06f62' - } - shade_network = defaultdict(dict) - - # 1% - commission = int(.01 * 10000) - # viewing_key = '4b734d7a2e71fb277a9e3355f5c56d347f1012e1a9533eb7fdbb3ceceedad5fc' - viewing_key = 'passsword' - - chain_config = secretlib.run_command(['secretcli', 'config']) - - chain_config = { - key.strip('" '): val.strip('" ') - for key, val in - ( - line.split('=') - for line in chain_config.split('\n') - if line - ) - } - - if chain_config['chain-id'] == 'holodeck-2': - account_key = 'drpresident' - backend = None - print('Setting testnet SSCRT') - sscrt = PreInstantiatedContract(TESTNET_SSCRT['address'], TESTNET_SSCRT['code_hash']) - sscrt.symbol = 'SSCRT' - print(sscrt.address) - - print('Setting testnet band') - band = PreInstantiatedContract(TESTNET_BAND['address'], TESTNET_BAND['code_hash']) - print(TESTNET_BAND['address']) - - elif chain_config['chain-id'] == 'enigma-pub-testnet-3': - account_key = 'a' - backend = 'test' - print('Configuring SSCRT') - sscrt = SNIP20(gen_label(8), name='secretSCRT', symbol='SSCRT', decimals=6, public_total_supply=True, enable_deposit=True, enable_burn=True, - admin=account_key, uploader=account_key, backend=backend) - print(sscrt.address) - sscrt.set_view_key(account_key, viewing_key) - - print('Mocking Band for local chain') - band = Contract('mock_band.wasm.gz', '{}', gen_label(8)) - print(band.address) - - else: - print('Failed to determine chain', chain_config['chain-id']) - exit(1) - - - - account = secretlib.run_command(['secretcli', 'keys', 'show', '-a', account_key]).rstrip() - - print(json.loads(secretlib.run_command(['secretcli', 'q', 'account', account]))['value']['coins']) - - print('Configuring Oracle') - oracle = Oracle(gen_label(8), band, - admin=account_key, uploader=account_key, backend=backend) - shade_network['oracle'] = { - 'address': oracle.address, - 'code_hash': oracle.code_hash, - } - print(oracle.address) - scrt_price = int(oracle.get_price('SCRT')['rate']) - print('SCRT', scrt_price / (10 ** 18)) - sscrt_price = int(oracle.get_price('SSCRT')['rate']) - print('SSCRT', scrt_price / (10 ** 18)) - for _, symbol in core_assets + synthetic_assets: - print(symbol, int(oracle.get_price(symbol)['rate']) / (10 ** 18)) - - print('Configuring Treasury') - treasury = Treasury(gen_label(8), - admin=account_key, uploader=account_key, backend=backend) - print(treasury.address) - shade_network['treasury'] = { - 'address': treasury.address, - 'code_hash': treasury.code_hash, - } - - print('Registering SSCRT with treasury') - print(treasury.register_asset(sscrt)) - - snip20_id = None - mint_id = None - - shade_network['assets'] = {} - print('\nCore Snips') - core_snips = [] - for name, symbol in core_assets: - print('\nConfiguring', name, symbol) - snip = SNIP20(gen_label(8), name=name, symbol=symbol, decimals=6, - public_total_supply=True, enable_mint=True, enable_burn=True, - admin=account_key, uploader=account_key, backend=backend, - code_id=snip20_id) - if not snip20_id: - snip20_id = snip.code_id - - shade_network['assets'][snip.symbol]['snip20'] = { - 'address': snip.address, - 'code_hash': snip.code_hash, - } - - print(f'Registering {snip.symbol} with treasury') - treasury.register_asset(snip) - - print('Set Viewing Key') - snip.set_view_key(account_key, viewing_key) - print(snip.symbol, snip.address) - core_snips.append(snip) - - print('\nSynthetic Snips') - - synthetic_snips = [] - # (snip, mint) - synthetics = [] - - for name, symbol in synthetic_assets: - print('\nConfiguring', name, symbol) - snip = SNIP20(gen_label(8), name=name, symbol=symbol, - decimals=6, public_total_supply=True, - enable_mint=True, enable_burn=True, - admin=account_key, uploader=account_key, backend=backend, - code_id=snip20_id) - shade_network['assets'][snip.symbol]['snip20'] = { - 'address': snip.address, - 'code_hash': snip.code_hash, - } - print(f'Registering {snip.symbol} with treasury') - treasury.register_asset(snip) - print('Set Viewing Key') - snip.set_view_key(account_key, viewing_key) - print(snip.symbol, snip.address) - - synthetic_snips.append(snip) - - print('Core Mints') - # (snip, mint) - core_pairs = [] - for snip in core_snips: - initial_assets = [ - c - for c in core_snips - if c is not snip - ] + synthetic_snips + [ sscrt ] - print('Configuring', snip.symbol, 'Mint') - mint = MicroMint(gen_label(8), snip, oracle, treasury, commission, - # initial_assets=initial_assets, - admin=account_key, uploader=account_key, backend=backend, - code_id=mint_id) - if not mint_id: - mint_id = mint.code_id - - shade_network['assets'][snip.symbol]['mint'] = { - 'address': mint.address, - 'code_hash': mint.code_hash, - } - print(mint.address) - - print(f'Linking Snip/Mint {snip.symbol}') - snip.set_minters([mint.address]) - - for i in initial_assets: - print(f'Registering {i.symbol} with {snip.symbol}') - mint.register_asset(i) - - core_pairs.append((snip, mint)) - - print('Synthetic Mints') - for snip in synthetic_snips: - initial_assets = [ - s - for s in synthetic_snips - if s is not snip - ] + core_snips - print('Configuring', snip.symbol, 'Mint') - mint = MicroMint(gen_label(8), snip, oracle, treasury, commission, - # initial_assets=initial_assets, - admin=account_key, uploader=account_key, backend=backend, - code_id=mint_id) - - shade_network['assets'][snip.symbol]['mint'] = { - 'address': mint.address, - 'code_hash': mint.code_hash, - } - print(mint.address) - - for i in initial_assets: - print(f'Registering {i.symbol} with {snip.symbol}') - mint.register_asset(i) - - print(f'Linking Snip/Mint {snip.symbol}') - snip.set_minters([mint.address]) - - synthetics.append((snip, mint)) - - print(secretlib.run_command(['secretcli', 'q', 'account', account])) - - if not no_prime: - - sscrt_mint_amount = '100000000000000' - print(f'\tDeposit {sscrt_mint_amount} uSCRT') - sscrt.deposit(account, sscrt_mint_amount + 'uscrt') - sscrt_minted = sscrt.get_balance(account, viewing_key) - print(f'\tReceived {sscrt_minted} usSCRT') - assert sscrt_mint_amount == sscrt_minted, f'Minted {sscrt_minted}; expected {sscrt_mint_amount}' - total_amount = 100000000000000 - minimum_amount = 1000 - send_amount = random.randint(1000, int(total_amount / len(core_pairs + synthetics)) - 1) - - # Initializing balances with sscrt - print('\nEntry minting with SSCRT') - for core, mint in core_pairs: - print(f'Burning {send_amount} usSCRT for', core.symbol) - - mint_response = sscrt.send(account_key, mint.address, send_amount) - # print(mint_response) - if mint_response.get('output_error'): - print(f'Mint error: {mint_response["output_error"]}') - - print('Wallet Balance') - for snip in core_snips + synthetic_snips: - print('\t', snip.get_balance(account, viewing_key), snip.symbol) - - print('Treasury Balance') - print('\t', treasury.get_balance(sscrt), sscrt.symbol) - for snip in core_snips + synthetic_snips: - print('\t', treasury.get_balance(snip), snip.symbol) - - print('\nBurning core assets for eachother') - - for core, _ in core_pairs: - send_amount = int( - int(core.get_balance(account, viewing_key)) - / 10) - for c, mint in core_pairs: - if c is core: - continue - print(f'\nBurning {send_amount}', core.symbol, 'for', c.symbol) - - mint_response = core.send(account_key, mint.address, send_amount) - # print(mint_response) - if mint_response.get('output_error'): - print(f'Mint error: {mint_response["output_error"]}') - - - print('\nBurning core assets for Synthetics') - #send_amount = int(send_amount / 3) - for core, _ in core_pairs: - send_amount = int( - int(core.get_balance(account, viewing_key)) - / (len(synthetics) + 1)) - - for synthetic, mint in synthetics: - print(f'\nBurning {send_amount}', core.symbol, 'for', synthetic.symbol) - - mint_response = core.send(account_key, mint.address, send_amount) - # print(mint_response) - if mint_response.get('output_error'): - print(f'Mint error: {mint_response["output_error"]}') - - print('\nBurning Synthetics amongst eachother') - - for snip in synthetic_snips: - send_amount = int( - int(snip.get_balance(account, viewing_key)) - / (len(synthetic_snips) + 1)) - - for s, mint in synthetics: - if s is snip: - continue - print(f'\nBurning {send_amount}', synthetic.symbol, 'for', s.symbol) - - mint_response = synthetic.send(account_key, mint.address, send_amount) - print(mint_response) - if mint_response.get('output_error'): - print(f'Mint error: {mint_response["output_error"]}') - - - print('Wallet Balance') - for snip in core_snips + synthetic_snips: - print('\t', snip.get_balance(account, viewing_key), snip.symbol) - - print('Treasury Balance') - print('\t', treasury.get_balance(sscrt), sscrt.symbol) - for snip in core_snips + synthetic_snips: - print('\t', treasury.get_balance(snip), snip.symbol) - - print(json.dumps(shade_network, indent=2)) - suffix = 'local' if account_key == 'a' else 'testnet' - - print(len(secretlib.GAS_METRICS), 'times gas was used') - open(f'logs/gas_metrics_{suffix}.json', 'w+').write(json.dumps(secretlib.GAS_METRICS, indent=2)) - - shade_network['gas_wanted'] = sum(int(g['want']) for g in secretlib.GAS_METRICS) - shade_network['gas_used'] = sum(int(g['used']) for g in secretlib.GAS_METRICS) - open(f'logs/shade_{suffix}.json', 'w+').write(json.dumps(shade_network, indent=2)) - return shade_network - -if __name__ == '__main__': - shade_network = deploy_network() - diff --git a/makefile b/makefile index 8b36baa64..b7a3a7502 100755 --- a/makefile +++ b/makefile @@ -2,21 +2,63 @@ contracts_dir=contracts compiled_dir=compiled checksum_dir=${compiled_dir}/checksum -CONTRACTS = mint snip20 treasury micro_mint oracle mock_band initializer +# Compresses the wasm file, args: compressed_file_name, built_file_name +define compress_wasm = +(cd $(contracts_dir)/$(1); cargo unit-test) +wasm-opt -Oz ./target/wasm32-unknown-unknown/release/$(2).wasm -o ./$(1).wasm +echo $(md5sum $(1).wasm | cut -f 1 -d " ") >> ${checksum_dir}/$(1).txt +cat ./$(1).wasm | gzip -n -9 > ${compiled_dir}/$(1).wasm.gz +rm ./$(1).wasm +endef -COMPILED = ${CONTRACTS:=.wasm.gz} +CONTRACTS = airdrop governance staking mint treasury micro_mint oracle mock_band initializer scrt_staking -all: setup $(CONTRACTS) +release: build_release compress + +debug: build_debug compress + +build_release: + (cd ${contracts_dir}; RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --locked) + +build_debug: + (cd ${contracts_dir}; RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --locked --features="debug-print") + +compress: setup $(CONTRACTS); $(call compress_wasm,snip20,snip20_reference_impl) $(CONTRACTS): - (cd $(contracts_dir)/$@; cargo unit-test) - (cd ${contracts_dir}; cargo build --release --target wasm32-unknown-unknown --locked) - wasm-opt -Oz ./target/wasm32-unknown-unknown/release/$@.wasm -o ./$@.wasm - echo $(md5sum $@.wasm | cut -f 1 -d " ") >> ${checksum_dir}/$@.txt - cat ./$@.wasm | gzip -n -9 > ${compiled_dir}/$@.wasm.gz - rm ./$@.wasm + $(call compress_wasm,$@,$@) setup: $(compiled_dir) $(checksum_dir) $(compiled_dir) $(checksum_dir): mkdir $@ + +check: + cargo check + +clippy: + cargo clippy + +clean: + rm -r $(compiled_dir) + +format: + cargo fmt + +# Downloads the docker server +server-download: + docker pull securesecrets/sn-testnet:v0.2 + +# Starts the docker server / private testnet +server-start: + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1337:1337 \ + -v $$(pwd):/root/code --name shade-testnet securesecrets/sn-testnet:v0.2 + +# Connects to the docker server +server-connect: + docker exec -it shade-testnet /bin/bash + +# Runs integration tests +integration-tests: + cargo test -- --nocapture --test-threads=1 \ No newline at end of file diff --git a/packages/network_integration/Cargo.toml b/packages/network_integration/Cargo.toml new file mode 100644 index 000000000..1fea3764d --- /dev/null +++ b/packages/network_integration/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "network-integration" +version = "0.1.0" +authors = ["Guy Garcia "] +edition = "2018" + +[[bin]] +name = "testnet_airdrop" +path = "src/testnet_airdrop.rs" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] + +[dependencies] +colored = "2.0.0" +chrono = "0.4.19" +shade-protocol = { version = "0.1.0", path = "../shade_protocol" } +secretcli = { version = "0.1.0", path = "../secretcli" } +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.67"} +getrandom = { version = "0.2", features = ["js"] } # Prevents wasm from freaking out when running make +rand = { version = "0.8.4"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +rs_merkle = { git = "https://github.com/FloppyDisck/rs-merkle", branch = "node_export" } +flexible-permits = {git = "https://github.com/securesecrets/flexible-permits", tag = "v1.0.1"} + diff --git a/packages/network_integration/src/contract_helpers/governance.rs b/packages/network_integration/src/contract_helpers/governance.rs new file mode 100644 index 000000000..cd79e58a9 --- /dev/null +++ b/packages/network_integration/src/contract_helpers/governance.rs @@ -0,0 +1,174 @@ +use cosmwasm_std::{HumanAddr, Uint128}; +use serde_json::Result; +use shade_protocol::{asset::Contract, governance, governance::GOVERNANCE_SELF}; + +use crate::utils::{ + generate_label, + print_contract, + print_header, + print_warning, + ACCOUNT_KEY, + GAS, + STORE_GAS, +}; + +use secretcli::{ + cli_types::NetContract, + secretcli::{query_contract, test_contract_handle, test_inst_init}, +}; + +pub fn init_contract( + governance: &NetContract, + contract_name: String, + contract_path: &str, + contract_init: Init, +) -> Result { + print_header(&format!("{}{}", "Initializing ", contract_name)); + + let contract = test_inst_init( + &contract_init, + contract_path, + &*generate_label(8), + ACCOUNT_KEY, + Some(STORE_GAS), + Some(GAS), + Some("test"), + )?; + + print_contract(&contract); + + add_contract(contract_name, &contract, governance)?; + + Ok(contract) +} + +pub fn get_contract(governance: &NetContract, target: String) -> Result { + let msg = governance::QueryMsg::GetSupportedContract { name: target }; + + let query: governance::QueryAnswer = query_contract(governance, &msg)?; + + let mut ctrc = Contract { + address: HumanAddr::from("not_found".to_string()), + code_hash: "not_found".to_string(), + }; + + if let governance::QueryAnswer::SupportedContract { contract } = query { + ctrc = contract; + } + + Ok(ctrc) +} + +pub fn add_contract(name: String, target: &NetContract, governance: &NetContract) -> Result<()> { + print_warning(&format!("{}{}", "Adding ", name)); + + let msg = governance::HandleMsg::AddSupportedContract { + name: name.clone(), + contract: Contract { + address: HumanAddr::from(target.address.clone()), + code_hash: target.code_hash.clone(), + }, + }; + + create_and_trigger_proposal( + governance, + GOVERNANCE_SELF.to_string(), + &msg, + Some("Add a contract"), + )?; + + { + let query_msg = governance::QueryMsg::GetSupportedContract { name }; + + let query: governance::QueryAnswer = query_contract(governance, query_msg)?; + + if let governance::QueryAnswer::SupportedContract { contract } = query { + assert_eq!(contract.address.to_string(), target.address.to_string()); + assert_eq!(contract.code_hash, target.code_hash); + } else { + assert!(false, "Query returned unexpected type"); + } + } + + Ok(()) +} + +/// Assumes that governance's staker is not activated +pub fn create_and_trigger_proposal( + governance: &NetContract, + target: String, + handle: Handle, + desc: Option<&str>, +) -> Result { + create_proposal(governance, target, handle, desc)?; + + trigger_latest_proposal(governance) +} + +pub fn get_latest_proposal(governance: &NetContract) -> Result { + let query_msg = governance::QueryMsg::GetTotalProposals {}; + + let query: governance::QueryAnswer = query_contract(governance, &query_msg)?; + + let mut proposals = Uint128(1); + + if let governance::QueryAnswer::TotalProposals { total } = query { + proposals = total; + } else { + assert!(false, "Query returned unexpected type") + } + + Ok(proposals) +} + +pub fn create_proposal( + governance: &NetContract, + target: String, + handle: Handle, + desc: Option<&str>, +) -> Result<()> { + let msg = serde_json::to_string(&handle)?; + + let proposal_msg = governance::HandleMsg::CreateProposal { + target_contract: target, + proposal: msg, + description: match desc { + None => "Custom proposal".to_string(), + Some(description) => description.to_string(), + }, + }; + + //let proposals = get_latest_proposal(governance)?; + + test_contract_handle( + &proposal_msg, + governance, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + //assert_eq!(proposals, get_latest_proposal(governance)?); + + Ok(()) +} + +pub fn trigger_latest_proposal(governance: &NetContract) -> Result { + let proposals = get_latest_proposal(governance)?; + + let handle_msg = governance::HandleMsg::TriggerProposal { + proposal_id: proposals, + }; + + test_contract_handle( + &handle_msg, + governance, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + Ok(proposals) +} diff --git a/packages/network_integration/src/contract_helpers/initializer.rs b/packages/network_integration/src/contract_helpers/initializer.rs new file mode 100644 index 000000000..06799e56b --- /dev/null +++ b/packages/network_integration/src/contract_helpers/initializer.rs @@ -0,0 +1,121 @@ +use crate::{ + contract_helpers::minter::get_balance, + utils::{ + generate_label, + print_contract, + print_header, + print_warning, + ACCOUNT_KEY, + GAS, + INITIALIZER_FILE, + STORE_GAS, + VIEW_KEY, + }, +}; +use cosmwasm_std::{HumanAddr, Uint128}; +use secretcli::{ + cli_types::NetContract, + secretcli::{list_contracts_by_code, test_contract_handle, test_inst_init}, +}; +use serde_json::Result; +use shade_protocol::{ + initializer, + initializer::Snip20ContractInfo, + snip20, + snip20::InitialBalance, +}; + +pub fn initialize_initializer( + admin: String, + sscrt: &NetContract, + account: String, +) -> Result<(NetContract, NetContract, NetContract)> { + print_header("Initializing Initializer"); + let mut shade = NetContract { + label: generate_label(8), + id: "".to_string(), + address: "".to_string(), + code_hash: sscrt.code_hash.clone(), + }; + + let mut silk = NetContract { + label: generate_label(8), + id: "".to_string(), + address: "".to_string(), + code_hash: sscrt.code_hash.clone(), + }; + + let init_msg = initializer::InitMsg { + snip20_id: sscrt.id.parse::().unwrap(), + snip20_code_hash: sscrt.code_hash.clone(), + shade: Snip20ContractInfo { + label: shade.label.clone(), + admin: Some(HumanAddr::from(admin.clone())), + prng_seed: Default::default(), + initial_balances: Some(vec![InitialBalance { + address: HumanAddr::from(account.clone()), + amount: Uint128(10000000), + }]), + }, + silk: Snip20ContractInfo { + label: silk.label.clone(), + admin: Some(HumanAddr::from(admin)), + prng_seed: Default::default(), + initial_balances: None, + }, + }; + + let initializer = test_inst_init( + &init_msg, + INITIALIZER_FILE, + &*generate_label(8), + ACCOUNT_KEY, + Some(STORE_GAS), + Some(GAS), + Some("test"), + )?; + print_contract(&initializer); + + print_header("Getting uploaded Snip20s"); + + let contracts = list_contracts_by_code(sscrt.id.clone())?; + + for contract in contracts { + if contract.label == shade.label { + print_warning("Found Shade"); + shade.id = contract.code_id.to_string(); + shade.address = contract.address; + print_contract(&shade); + } else if contract.label == silk.label { + print_warning("Found Silk"); + silk.id = contract.code_id.to_string(); + silk.address = contract.address; + print_contract(&silk); + } + } + + // Set View keys + { + let msg = snip20::HandleMsg::SetViewingKey { + key: String::from(VIEW_KEY), + padding: None, + }; + + test_contract_handle(&msg, &shade, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + println!("\n\tTotal shade: {}", get_balance(&shade, account.clone())); + + { + let msg = snip20::HandleMsg::SetViewingKey { + key: String::from(VIEW_KEY), + padding: None, + }; + + test_contract_handle(&msg, &silk, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + println!("\tTotal silk: {}", get_balance(&silk, account)); + + Ok((initializer, shade, silk)) +} diff --git a/packages/network_integration/src/contract_helpers/minter.rs b/packages/network_integration/src/contract_helpers/minter.rs new file mode 100644 index 000000000..d07a8d14b --- /dev/null +++ b/packages/network_integration/src/contract_helpers/minter.rs @@ -0,0 +1,187 @@ +use crate::{ + contract_helpers::governance::{create_and_trigger_proposal, get_contract, init_contract}, + utils::{ + print_contract, + print_epoch_info, + print_header, + print_vec, + GAS, + MICRO_MINT_FILE, + VIEW_KEY, + }, +}; +use cosmwasm_std::{to_binary, HumanAddr, Uint128}; +use secretcli::{ + cli_types::NetContract, + secretcli::{query_contract, test_contract_handle}, +}; +use serde_json::Result; +use shade_protocol::{asset::Contract, micro_mint, mint, snip20}; + +pub fn initialize_minter( + governance: &NetContract, + contract_name: String, + native_asset: &Contract, +) -> Result { + let minter = init_contract( + governance, + contract_name, + MICRO_MINT_FILE, + micro_mint::InitMsg { + admin: Some(HumanAddr::from(governance.address.clone())), + native_asset: native_asset.clone(), + oracle: get_contract(governance, "oracle".to_string())?, + peg: None, + treasury: None, + secondary_burn: None, + start_epoch: None, + epoch_frequency: Some(Uint128(120)), + epoch_mint_limit: Some(Uint128(1000000000)), + }, + )?; + + print_contract(&minter); + + print_epoch_info(&minter); + + Ok(minter) +} + +pub fn setup_minters( + governance: &NetContract, + mint_shade: &NetContract, + mint_silk: &NetContract, + shade: &Contract, + silk: &Contract, + sscrt: &NetContract, +) -> Result<()> { + print_header("Registering allowed tokens in mint contracts"); + create_and_trigger_proposal( + governance, + "shade_minter".to_string(), + micro_mint::HandleMsg::RegisterAsset { + contract: Contract { + address: HumanAddr::from(sscrt.address.clone()), + code_hash: sscrt.code_hash.clone(), + }, + capture: Some(Uint128(1000)), + }, + Some("Register asset"), + )?; + create_and_trigger_proposal( + governance, + "shade_minter".to_string(), + micro_mint::HandleMsg::RegisterAsset { + contract: silk.clone(), + capture: Some(Uint128(1000)), + }, + Some("Register asset"), + )?; + create_and_trigger_proposal( + governance, + "silk_minter".to_string(), + micro_mint::HandleMsg::RegisterAsset { + contract: shade.clone(), + capture: Some(Uint128(1000)), + }, + Some("Register asset"), + )?; + + print_header("Adding allowed minters in Snip20s"); + + create_and_trigger_proposal( + governance, + "shade".to_string(), + snip20::HandleMsg::SetMinters { + minters: vec![HumanAddr::from(mint_shade.address.clone())], + padding: None, + }, + Some("Set minters"), + )?; + + { + let msg = snip20::QueryMsg::Minters {}; + + let query: snip20::QueryAnswer = query_contract( + &NetContract { + label: "".to_string(), + id: "".to_string(), + address: shade.address.clone().to_string(), + code_hash: shade.code_hash.clone(), + }, + &msg, + )?; + + if let snip20::QueryAnswer::Minters { minters } = query { + print_vec("Shade minters: ", minters); + } + } + + create_and_trigger_proposal( + governance, + "silk".to_string(), + snip20::HandleMsg::SetMinters { + minters: vec![HumanAddr::from(mint_silk.address.clone())], + padding: None, + }, + Some("Set minters"), + )?; + + { + let msg = snip20::QueryMsg::Minters {}; + + let query: snip20::QueryAnswer = query_contract( + &NetContract { + label: "".to_string(), + id: "".to_string(), + address: silk.address.clone().to_string(), + code_hash: silk.code_hash.clone(), + }, + &msg, + )?; + if let snip20::QueryAnswer::Minters { minters } = query { + print_vec("Silk minters: ", minters); + } + } + + Ok(()) +} + +pub fn get_balance(contract: &NetContract, from: String) -> Uint128 { + let msg = snip20::QueryMsg::Balance { + address: HumanAddr::from(from), + key: String::from(VIEW_KEY), + }; + + let balance: snip20::QueryAnswer = query_contract(contract, &msg).unwrap(); + + if let snip20::QueryAnswer::Balance { amount } = balance { + return amount; + } + + Uint128(0) +} + +pub fn mint( + snip: &NetContract, + sender: &str, + minter: String, + amount: Uint128, + minimum_expected: Uint128, + backend: &str, +) { + let msg = snip20::HandleMsg::Send { + recipient: HumanAddr::from(minter), + amount, + msg: Some( + to_binary(&mint::MintMsgHook { + minimum_expected_amount: minimum_expected, + }) + .unwrap(), + ), + memo: None, + padding: None, + }; + + test_contract_handle(&msg, snip, sender, Some(GAS), Some(backend), None).unwrap(); +} diff --git a/packages/network_integration/src/contract_helpers/mod.rs b/packages/network_integration/src/contract_helpers/mod.rs new file mode 100644 index 000000000..78537acd7 --- /dev/null +++ b/packages/network_integration/src/contract_helpers/mod.rs @@ -0,0 +1,4 @@ +pub mod governance; +pub mod initializer; +pub mod minter; +pub mod stake; diff --git a/packages/network_integration/src/contract_helpers/stake.rs b/packages/network_integration/src/contract_helpers/stake.rs new file mode 100644 index 000000000..9cd6e9423 --- /dev/null +++ b/packages/network_integration/src/contract_helpers/stake.rs @@ -0,0 +1,207 @@ +use crate::{ + contract_helpers::{governance::init_contract, minter::get_balance}, + utils::{print_contract, print_header, ACCOUNT_KEY, GAS, STAKING_FILE}, +}; +use cosmwasm_std::{HumanAddr, Uint128}; +use secretcli::{ + cli_types::NetContract, + secretcli::{query_contract, test_contract_handle}, +}; +use serde_json::Result; +use shade_protocol::{asset::Contract, snip20, staking}; +use std::{thread, time, time::UNIX_EPOCH}; + +pub fn setup_staker( + governance: &NetContract, + shade: &Contract, + staking_account: String, +) -> Result { + let staker = init_contract( + governance, + "staking".to_string(), + STAKING_FILE, + staking::InitMsg { + admin: Some(Contract { + address: HumanAddr::from(governance.address.clone()), + code_hash: governance.code_hash.clone(), + }), + unbond_time: 180, + staked_token: Contract { + address: shade.address.clone(), + code_hash: shade.code_hash.clone(), + }, + }, + )?; + + print_contract(&staker); + + let shade_net = NetContract { + label: "-".to_string(), + id: "-".to_string(), + address: shade.address.to_string(), + code_hash: shade.code_hash.clone(), + }; + + print_header("Testing staking delegation"); + + // Query current balance + let original_balance = get_balance(&shade_net, staking_account.clone()); + let stake_amount = Uint128(7000000); + let unbond_amount = Uint128(2000000); + let balance_after_stake = (original_balance - stake_amount).unwrap(); + + // Make a query key + { + let msg = staking::HandleMsg::SetViewingKey { + key: "password".to_string(), + }; + + test_contract_handle(&msg, &staker, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + // Stake some Shade on it + { + let msg = snip20::HandleMsg::Send { + recipient: HumanAddr::from(staker.address.clone()), + amount: stake_amount, + msg: None, + memo: None, + padding: None, + }; + + test_contract_handle(&msg, &shade_net, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + // Check total stake + assert_eq!(get_total_staked(&staker), stake_amount); + + // Check user stake + assert_eq!( + get_user_stake(&staker, staking_account.clone(), "password".to_string()).staked, + stake_amount + ); + + // Query total Shade now + assert_eq!( + balance_after_stake, + get_balance(&shade_net, staking_account.clone()) + ); + + print_header("Testing unbonding request"); + // User unbonds + { + let msg = staking::HandleMsg::Unbond { + amount: unbond_amount, + }; + + test_contract_handle(&msg, &staker, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + // Check if unstaking + assert_eq!( + get_total_staked(&staker), + (stake_amount - unbond_amount).unwrap() + ); + + // Check if user unstaking + { + let user_stake = get_user_stake(&staker, staking_account.clone(), "password".to_string()); + + assert_eq!(user_stake.staked, (stake_amount - unbond_amount).unwrap()); + assert_eq!(user_stake.unbonding, unbond_amount); + } + + print_header("Testing unbonding time"); + // User triggers but receives nothing + { + let msg = staking::HandleMsg::ClaimUnbond {}; + + test_contract_handle(&msg, &staker, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + // Query total Shade now + + assert_eq!( + balance_after_stake, + get_balance(&shade_net, staking_account.clone()) + ); + + // Wait unbonding time + thread::sleep(time::Duration::from_secs(180)); + + // Check if unbonded + assert_eq!( + get_user_stake(&staker, staking_account.clone(), "password".to_string()).unbonded, + unbond_amount + ); + + print_header("Testing unbonding asset release"); + // User triggers and receives something + { + let msg = staking::HandleMsg::ClaimUnbond {}; + + test_contract_handle(&msg, &staker, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + // Query total Shade now + assert_eq!( + (balance_after_stake + unbond_amount), + get_balance(&shade_net, staking_account) + ); + + Ok(staker) +} + +pub fn get_total_staked(staker: &NetContract) -> Uint128 { + let msg = staking::QueryMsg::TotalStaked {}; + + let total_stake: staking::QueryAnswer = query_contract(staker, &msg).unwrap(); + + if let staking::QueryAnswer::TotalStaked { total } = total_stake { + return total; + } + + Uint128::zero() +} + +pub struct TestUserStake { + pub staked: Uint128, + pub pending_rewards: Uint128, + pub unbonding: Uint128, + pub unbonded: Uint128, +} + +pub fn get_user_stake(staker: &NetContract, address: String, key: String) -> TestUserStake { + let msg = staking::QueryMsg::UserStake { + address: HumanAddr::from(address), + key, + time: time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("") + .as_secs(), + }; + + let query: staking::QueryAnswer = query_contract(staker, &msg).unwrap(); + + if let staking::QueryAnswer::UserStake { + staked, + pending_rewards, + unbonding, + unbonded, + } = query + { + return TestUserStake { + staked, + pending_rewards, + unbonding, + unbonded, + }; + } + + TestUserStake { + staked: Uint128::zero(), + pending_rewards: Uint128::zero(), + unbonding: Uint128::zero(), + unbonded: Uint128::zero(), + } +} diff --git a/packages/network_integration/src/lib.rs b/packages/network_integration/src/lib.rs new file mode 100644 index 000000000..1c845cc7a --- /dev/null +++ b/packages/network_integration/src/lib.rs @@ -0,0 +1,2 @@ +pub mod contract_helpers; +pub mod utils; diff --git a/packages/network_integration/src/run.rs b/packages/network_integration/src/run.rs new file mode 100644 index 000000000..6ef552ae4 --- /dev/null +++ b/packages/network_integration/src/run.rs @@ -0,0 +1,345 @@ +use colored::*; +use serde_json::Result; +use std::fmt::Display; +use serde::Serialize; +use rand::{distributions::Alphanumeric, Rng}; +use secretcli::{cli_types::NetContract, + secretcli::{account_address, TestInit, TestHandle, + TestQuery, list_contracts_by_code}}; +use shade_protocol::{initializer::{Snip20ContractInfo}, micro_mint, + snip20::{InitConfig, InitialBalance}, oracle, + band, snip20, initializer, mint, asset::Contract, micro_mint::MintLimit}; +use cosmwasm_std::{HumanAddr, Uint128, to_binary}; + +const STORE_GAS: &str = "10000000"; +const GAS: &str = "800000"; +const VIEW_KEY: &str = "password"; +const ACCOUNT_KEY: &str = "a"; + +fn generate_label(size: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(size) + .map(char::from) + .collect() +} + +fn main() -> Result<()> { + let account = account_address(ACCOUNT_KEY)?; + + println!("Using Account: {}", account.blue()); + + // Initialize sSCRT + print_header("Initializing sSCRT"); + let sSCRT = snip20::InitMsg { + name: "sSCRT".to_string(), + admin: None, + symbol: "SSCRT".to_string(), + decimals: 6, + initial_balances: None, + prng_seed: Default::default(), + config: Some(InitConfig { + public_total_supply: Some(true), + enable_deposit: Some(true), + enable_redeem: Some(true), + enable_mint: Some(true), + enable_burn: Some(false) + }) + }.inst_init("../../compiled/snip20.wasm.gz", &*generate_label(8), + ACCOUNT_KEY, Some(STORE_GAS), Some(GAS), + Some("test"))?; + print_contract(&sSCRT); + + snip20::HandleMsg::SetViewingKey { key: String::from(VIEW_KEY), padding: None }.t_handle( + &sSCRT, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + + println!("Depositing 1000000000uscrt"); + + snip20::HandleMsg::Deposit { padding: None }.t_handle(&sSCRT, ACCOUNT_KEY, + Some(GAS), Some("test"), + Some("1000000000uscrt"))?; + + println!("Total sSCRT: {}", get_balance(&sSCRT, account.clone())); + + // Initialize initializer + print_header("Initializing Initializer"); + let mut shade = NetContract { + label: generate_label(8), + id: "".to_string(), + address: "".to_string(), + code_hash: sSCRT.code_hash.clone() + }; + + let mut silk = NetContract { + label: generate_label(8), + id: "".to_string(), + address: "".to_string(), + code_hash: sSCRT.code_hash.clone() + }; + + let initializer = initializer::InitMsg { + snip20_id: sSCRT.id.parse::().unwrap(), + snip20_code_hash: sSCRT.code_hash.clone(), + shade: Snip20ContractInfo { + label: shade.label.clone(), + admin: None, + prng_seed: Default::default(), + initial_balances: Some(vec![InitialBalance{ address: HumanAddr::from(account.clone()), amount: Uint128(10000000) }]) + }, + silk: Snip20ContractInfo { + label: silk.label.clone(), + admin: None, + prng_seed: Default::default(), + initial_balances: None + } + }.inst_init("../../compiled/initializer.wasm.gz", &*generate_label(8), + ACCOUNT_KEY, Some(STORE_GAS), Some(GAS), + Some("test"))?; + print_contract(&initializer); + + + print_header("Getting uploaded Snip20s"); + + let contracts = list_contracts_by_code(sSCRT.id.clone())?; + + for contract in contracts { + if &contract.label == &shade.label { + print_warning("Found Shade"); + shade.id = contract.code_id.to_string(); + shade.address = contract.address; + print_contract(&shade); + } + else if &contract.label == &silk.label { + print_warning("Found Silk"); + silk.id = contract.code_id.to_string(); + silk.address = contract.address; + print_contract(&silk); + } + } + + // Set View keys + snip20::HandleMsg::SetViewingKey { key: String::from(VIEW_KEY), padding: None }.t_handle( + &shade, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + + println!("Total shade: {}", get_balance(&shade, account.clone())); + + snip20::HandleMsg::SetViewingKey { key: String::from(VIEW_KEY), padding: None }.t_handle( + &silk, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + + println!("Total silk: {}", get_balance(&silk, account.clone())); + + print_header("Initializing Band Mock"); + + let band = band::InitMsg {}.inst_init("../../compiled/mock_band.wasm.gz", + &*generate_label(8), ACCOUNT_KEY, + Some(STORE_GAS), Some(GAS), + Some("test"))?; + + print_contract(&band); + + print_header("Initializing Oracle"); + let oracle = oracle::InitMsg { + admin: None, + band: Contract { address: HumanAddr::from(band.address), code_hash: band.code_hash }, + sscrt: Contract { address: HumanAddr::from(sSCRT.address.clone()), + code_hash: sSCRT.code_hash.clone() } + }.inst_init("../../compiled/oracle.wasm.gz", &*generate_label(8), + ACCOUNT_KEY, Some(STORE_GAS), Some(GAS), + Some("test"))?; + + print_contract(&oracle); + + print_header("Initializing Mint-Shade"); + let mint_shade = micro_mint::InitMsg { + admin: None, + native_asset: Contract { address: HumanAddr::from(shade.address.clone()), + code_hash: shade.code_hash.clone() }, + oracle: Contract { address: HumanAddr::from(oracle.address.clone()), + code_hash: oracle.code_hash.clone() }, + peg: None, + treasury: None, + epoch_frequency: Some(Uint128(120)), + epoch_mint_limit: Some(Uint128(1000000000)), + }.inst_init("../../compiled/micro_mint.wasm.gz", &*generate_label(8), + ACCOUNT_KEY, Some(STORE_GAS), Some(GAS), + Some("test"))?; + + print_contract(&mint_shade); + + print_epoch_info(&mint_shade); + + print_header("Initializing Mint-Silk"); + let mint_silk = micro_mint::InitMsg { + admin: None, + native_asset: Contract { address: HumanAddr::from(silk.address.clone()), + code_hash: silk.code_hash.clone() }, + oracle: Contract { address: HumanAddr::from(oracle.address.clone()), + code_hash: oracle.code_hash.clone() }, + peg: None, + treasury: None, + epoch_frequency: Some(Uint128(120)), + epoch_mint_limit: Some(Uint128(1000000000)), + }.inst_init("../../compiled/micro_mint.wasm.gz", &*generate_label(8), + ACCOUNT_KEY, Some(STORE_GAS), Some(GAS), + Some("test"))?; + + print_contract(&mint_silk); + + print_epoch_info(&mint_silk); + + print_header("Registering allowed tokens"); + micro_mint::HandleMsg::RegisterAsset { contract: Contract { + address: HumanAddr::from(sSCRT.address.clone()), + code_hash: sSCRT.code_hash.clone() }, commission: Some(Uint128(1000)) }.t_handle( + &mint_shade, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + micro_mint::HandleMsg::RegisterAsset { contract: Contract { + address: HumanAddr::from(silk.address.clone()), + code_hash: silk.code_hash.clone() }, commission: Some(Uint128(1000)) }.t_handle( + &mint_shade, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + micro_mint::HandleMsg::RegisterAsset { contract: Contract { + address: HumanAddr::from(shade.address.clone()), + code_hash: shade.code_hash.clone() }, commission: Some(Uint128(1000)) }.t_handle( + &mint_silk, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + + { + let query: micro_mint::QueryAnswer = micro_mint::QueryMsg::GetSupportedAssets {}.t_query( + &mint_shade)?; + if let micro_mint::QueryAnswer::SupportedAssets { assets } = query { + print_vec("Shade allowed tokens: ", assets); + } + } + + { + let query: micro_mint::QueryAnswer = micro_mint::QueryMsg::GetSupportedAssets {}.t_query( + &mint_silk)?; + if let micro_mint::QueryAnswer::SupportedAssets { assets } = query { + print_vec("Silk allowed tokens: ", assets); + } + } + + print_header("Setting minters in snip20s"); + + snip20::HandleMsg::SetMinters { + minters: vec![HumanAddr::from(mint_shade.address.clone())], padding: None }.t_handle( + &shade, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + + { + let query: snip20::QueryAnswer = snip20::QueryMsg::Minters {}.t_query(&shade)?; + if let snip20::QueryAnswer::Minters { minters } = query { + print_vec("Shade minters: ", minters); + } + } + + snip20::HandleMsg::SetMinters { + minters: vec![HumanAddr::from(mint_silk.address.clone())], padding: None }.t_handle( + &silk, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + + { + let query: snip20::QueryAnswer = snip20::QueryMsg::Minters {}.t_query(&silk)?; + if let snip20::QueryAnswer::Minters { minters } = query { + print_vec("Silk minters: ", minters); + } + } + + print_header("Testing minting"); + + { + let amount = get_balance(&sSCRT, account.clone()); + println!("Burning {} usSCRT for Shade", amount.to_string().blue()); + + mint(&sSCRT, ACCOUNT_KEY, mint_shade.address.clone(), amount, + Uint128(0), "test"); + } + + { + let amount = get_balance(&shade, account.clone()); + println!("Minted {} uShade", amount.to_string().blue()); + print_epoch_info(&mint_shade); + } + + // Test Mint Limit + { + let amount = Uint128(1000000000); + println!("Burning {} uShade for Silk ", amount.to_string().blue()); + mint(&shade, ACCOUNT_KEY, mint_silk.address.clone(), amount, + Uint128(0), "test"); + print_epoch_info(&mint_silk); + println!("Minted {} uSilk", get_balance(&silk, account.clone())); + } + + // Try to send whats left + { + let amount = Uint128(10000000); + let expected_total = Uint128(1010000000); + while get_balance(&silk, account.clone()) != expected_total { + mint(&shade, ACCOUNT_KEY, mint_silk.address.clone(), amount, + Uint128(0), "test"); + } + print_epoch_info(&mint_silk); + println!("Finally minted {} uSilk", amount); + } + + Ok(()) +} + +fn print_header(header: &str) { + println!("{}", header.on_blue()); +} + +fn print_warning(warn: &str) { + println!("{}", warn.on_yellow()); +} + +fn print_contract(contract: &NetContract) { + println!("\tLabel: {}\n\tID: {}\n\tAddress: {}\n\tHash: {}", contract.label, contract.id, + contract.address, contract.code_hash); +} + +fn print_epoch_info(minter: &NetContract) { + println!("\tEpoch information"); + let query = micro_mint::QueryMsg::GetMintLimit {}.t_query(minter).unwrap(); + + if let micro_mint::QueryAnswer::MintLimit { limit } = query { + println!("\tFrequency: {}\n\tCapacity: {}\n\tTotal Minted: {}\n\tNext Epoch: {}", + limit.frequency, limit.mint_capacity, limit.total_minted, limit.next_epoch); + } +} + +fn print_struct(item: Printable) { + println!("{}", serde_json::to_string_pretty(&item).unwrap()); +} + +fn print_vec(prefix: &str, vec: Vec) { + for e in vec.iter().take(1) { + print!("{}{}", prefix, e); + } + for e in vec.iter().skip(1) { + print!(", {}", e); + } + println!(); +} + +fn get_balance(contract: &NetContract, from: String, ) -> Uint128 { + let balance: snip20::QueryAnswer = snip20::QueryMsg::Balance { + address: HumanAddr::from(from), + key: String::from(VIEW_KEY), + }.t_query(contract).unwrap(); + + if let snip20::QueryAnswer::Balance { amount } = balance { + return amount + } + + Uint128(0) +} + +fn mint(snip: &NetContract, sender: &str, minter: String, amount: Uint128, + minimum_expected: Uint128, backend: &str) { + snip20::HandleMsg::Send { + recipient: HumanAddr::from(minter), + amount, + msg: Some(to_binary(&mint::MintMsgHook { + minimum_expected_amount: minimum_expected}).unwrap()), + memo: None, + padding: None + }.t_handle(snip, sender, Some(GAS), Some(backend), None).unwrap(); +} \ No newline at end of file diff --git a/packages/network_integration/src/testnet_airdrop.rs b/packages/network_integration/src/testnet_airdrop.rs new file mode 100644 index 000000000..3a3d79fa3 --- /dev/null +++ b/packages/network_integration/src/testnet_airdrop.rs @@ -0,0 +1,168 @@ +use cosmwasm_std::{Binary, HumanAddr, Uint128}; +use network_integration::utils::{ + generate_label, + print_contract, + print_header, + AIRDROP_FILE, + GAS, + SNIP20_FILE, + STORE_GAS, +}; +use rs_merkle::{algorithms::Sha256, Hasher, MerkleTree}; +use secretcli::secretcli::{account_address, test_contract_handle, test_inst_init}; +use serde::{Deserialize, Serialize}; +use serde_json::Result; +use shade_protocol::{ + airdrop, + airdrop::claim_info::RequiredTask, + asset::Contract, + snip20, + snip20::{InitConfig, InitialBalance}, +}; +use std::{env, fs}; + +#[derive(Serialize, Deserialize)] +pub struct Reward { + pub address: String, + pub amount: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Args { + db_path: String, + initial_amount: Uint128, + max_amount: Uint128, + admin: String, + start_date: u64, + end_date: u64, + decay_start: u64, +} + +fn main() -> Result<()> { + let bin_args: Vec = env::args().collect(); + let args_file = fs::read_to_string(&bin_args.get(1).expect("No argument provided")) + .expect("Unable to read args"); + let args: Args = serde_json::from_str(&args_file)?; + + let account_addr = account_address(&args.admin)?; + + print_header("Importing DB"); + let file_data = fs::read_to_string(args.db_path).expect("Unable to read db"); + let rewards: Vec = serde_json::from_str(&file_data)?; + + print_header("Converting into merkle tree"); + let raw_leaves: Vec = rewards + .iter() + .map(|x| x.address.clone() + &x.amount) + .collect(); + let leaves: Vec<[u8; 32]> = raw_leaves + .iter() + .map(|x| Sha256::hash(x.as_bytes())) + .collect(); + + let merkle_tree = MerkleTree::::from_leaves(&leaves); + let root = merkle_tree.root().unwrap(); + + // Store the tree + print_header("Storing tree"); + let mut stored_tree: Vec> = vec![]; + for layer in merkle_tree.layers().iter() { + let mut new_layer: Vec = vec![]; + for node in layer.iter() { + new_layer.push(Binary(node.to_vec())); + } + stored_tree.push(new_layer); + } + println!("Merkle tree height: {}", merkle_tree.layers().len()); + //let serialized = to_binary(&stored_tree).unwrap(); + fs::write("merkle_tree.json", serde_json::to_string(&stored_tree)?) + .expect("Could not store merkle tree"); + + // Initialize snip20 + print_header("Initializing Snip20"); + + let snip_init_msg = snip20::InitMsg { + name: "SHD".to_string(), + admin: None, + symbol: "SHADE".to_string(), + decimals: 6, + initial_balances: Some(vec![InitialBalance { + address: HumanAddr::from(account_addr.clone()), + amount: args.initial_amount, + }]), + prng_seed: Default::default(), + config: Some(InitConfig { + public_total_supply: Some(true), + enable_deposit: Some(false), + enable_redeem: Some(false), + enable_mint: Some(true), + enable_burn: Some(true), + }), + }; + + let snip = test_inst_init( + &snip_init_msg, + SNIP20_FILE, + &*generate_label(8), + &args.admin, + Some(STORE_GAS), + Some(GAS), + None, + )?; + print_contract(&snip); + + // Initialize airdrop + print_header("Initializing airdrop"); + + let airdrop_init_msg = airdrop::InitMsg { + admin: None, + dump_address: Some(HumanAddr::from(account_addr.clone())), + airdrop_token: Contract { + address: HumanAddr::from(snip.address.clone()), + code_hash: snip.code_hash.clone(), + }, + airdrop_amount: args.initial_amount, + start_date: Some(args.start_date), + end_date: Some(args.end_date), + decay_start: Some(args.decay_start), + merkle_root: Binary(root.to_vec()), + total_accounts: leaves.len() as u32, + max_amount: args.max_amount, + default_claim: Uint128(20), + task_claim: vec![RequiredTask { + address: HumanAddr::from(account_addr), + percent: Uint128(50), + }], + query_rounding: Uint128(10000000000), + }; + + let airdrop = test_inst_init( + &airdrop_init_msg, + AIRDROP_FILE, + &*generate_label(8), + &args.admin, + Some(STORE_GAS), + Some(GAS), + None, + )?; + + print_contract(&airdrop); + + print_header("Funding airdrop"); + test_contract_handle( + &snip20::HandleMsg::Send { + recipient: HumanAddr::from(airdrop.address), + amount: args.initial_amount, + msg: None, + memo: None, + padding: None, + }, + &snip, + &args.admin, + Some(GAS), + None, + None, + )?; + + Ok(()) +} diff --git a/packages/network_integration/src/utils.rs b/packages/network_integration/src/utils.rs new file mode 100644 index 000000000..4dcd7417f --- /dev/null +++ b/packages/network_integration/src/utils.rs @@ -0,0 +1,72 @@ +use colored::*; +use rand::{distributions::Alphanumeric, Rng}; +use secretcli::{cli_types::NetContract, secretcli::query_contract}; +use serde::Serialize; +use shade_protocol::micro_mint; +use std::fmt::Display; + +// Smart contracts +pub const SNIP20_FILE: &str = "../../compiled/snip20.wasm.gz"; +pub const AIRDROP_FILE: &str = "../../compiled/airdrop.wasm.gz"; +pub const GOVERNANCE_FILE: &str = "../../compiled/governance.wasm.gz"; +pub const MOCK_BAND_FILE: &str = "../../compiled/mock_band.wasm.gz"; +pub const ORACLE_FILE: &str = "../../compiled/oracle.wasm.gz"; +pub const INITIALIZER_FILE: &str = "../../compiled/initializer.wasm.gz"; +pub const MICRO_MINT_FILE: &str = "../../compiled/micro_mint.wasm.gz"; +pub const STAKING_FILE: &str = "../../compiled/staking.wasm.gz"; + +pub const STORE_GAS: &str = "10000000"; +pub const GAS: &str = "800000"; +pub const VIEW_KEY: &str = "password"; +pub const ACCOUNT_KEY: &str = "a"; + +pub fn generate_label(size: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(size) + .map(char::from) + .collect() +} + +pub fn print_header(header: &str) { + println!("{}", header.on_blue()); +} + +pub fn print_warning(warn: &str) { + println!("{}", warn.on_yellow()); +} + +pub fn print_contract(contract: &NetContract) { + println!( + "\tLabel: {}\n\tID: {}\n\tAddress: {}\n\tHash: {}", + contract.label, contract.id, contract.address, contract.code_hash + ); +} + +pub fn print_epoch_info(minter: &NetContract) { + println!("\tEpoch information"); + let msg = micro_mint::QueryMsg::GetMintLimit {}; + + let query: micro_mint::QueryAnswer = query_contract(minter, &msg).unwrap(); + + if let micro_mint::QueryAnswer::MintLimit { limit } = query { + println!( + "\tFrequency: {}\n\tCapacity: {}\n\tTotal Minted: {}\n\tNext Epoch: {}", + limit.frequency, limit.mint_capacity, limit.total_minted, limit.next_epoch + ); + } +} + +pub fn print_struct(item: Printable) { + println!("{}", serde_json::to_string_pretty(&item).unwrap()); +} + +pub fn print_vec(prefix: &str, vec: Vec) { + for e in vec.iter().take(1) { + print!("{}{}", prefix, e); + } + for e in vec.iter().skip(1) { + print!(", {}", e); + } + println!(); +} diff --git a/packages/network_integration/tests/testnet_integration.rs b/packages/network_integration/tests/testnet_integration.rs new file mode 100644 index 000000000..7cb0e2d98 --- /dev/null +++ b/packages/network_integration/tests/testnet_integration.rs @@ -0,0 +1,1090 @@ +use colored::*; +use cosmwasm_std::{to_binary, Binary, HumanAddr, Uint128}; +use flexible_permits::{permit::Permit, transaction::PermitSignature}; +use network_integration::{ + contract_helpers::{ + governance::{ + add_contract, + create_and_trigger_proposal, + create_proposal, + get_contract, + get_latest_proposal, + init_contract, + trigger_latest_proposal, + }, + initializer::initialize_initializer, + minter::{get_balance, initialize_minter, setup_minters}, + stake::setup_staker, + }, + utils::{ + generate_label, + print_contract, + print_header, + print_vec, + print_warning, + ACCOUNT_KEY, + AIRDROP_FILE, + GAS, + GOVERNANCE_FILE, + MOCK_BAND_FILE, + ORACLE_FILE, + SNIP20_FILE, + STORE_GAS, + VIEW_KEY, + }, +}; +use rs_merkle::{algorithms::Sha256, Hasher, MerkleTree}; +use secretcli::secretcli::{ + account_address, + create_permit, + query_contract, + test_contract_handle, + test_inst_init, +}; +use serde::Serialize; +use serde_json::Result; +use shade_protocol::{ + airdrop::{ + self, + account::{AccountPermitMsg, AddressProofMsg}, + claim_info::RequiredTask, + }, + asset::Contract, + band, + generic_response::ResponseStatus, + governance, + governance::{ + proposal::ProposalStatus, + vote::{UserVote, Vote}, + }, + oracle, + snip20::{self, InitConfig, InitialBalance}, + staking, +}; +use std::{thread, time}; + +fn create_signed_permit(permit_msg: T, signer: &str) -> Permit { + let chain_id = Some("testnet".to_string()); + let unsigned_msg = flexible_permits::transaction::SignedTx::from_msg( + flexible_permits::transaction::TxMsg { + r#type: "signature_proof".to_string(), + value: permit_msg.clone(), + }, + chain_id.clone(), + ); + + let signed_info = create_permit(unsigned_msg, signer).unwrap(); + + let permit = Permit { + params: permit_msg, + chain_id, + signature: PermitSignature { + pub_key: flexible_permits::transaction::PubKey { + r#type: signed_info.pub_key.msg_type, + value: Binary::from_base64(&signed_info.pub_key.value).unwrap(), + }, + signature: Binary::from_base64(&signed_info.signature).unwrap(), + }, + }; + + permit +} + +fn proof_from_tree(indices: &Vec, tree: &Vec>) -> Vec { + let mut current_indices: Vec = indices.clone(); + let mut helper_nodes: Vec = Vec::new(); + + for layer in tree { + let mut siblings: Vec = Vec::new(); + let mut parents: Vec = Vec::new(); + + for index in current_indices.iter() { + if index % 2 == 0 { + siblings.push(index + 1); + parents.push(index / 2); + } else { + siblings.push(index - 1); + parents.push((index - 1) / 2); + } + } + + for sibling in siblings { + if !current_indices.contains(&sibling) { + if let Some(item) = layer.get(sibling) { + helper_nodes.push(Binary(item.to_vec())); + } + } + } + + parents.dedup(); + current_indices = parents.clone(); + } + + helper_nodes +} + +#[test] +fn run_airdrop() -> Result<()> { + let account_a = account_address(ACCOUNT_KEY)?; + let account_b = account_address("b")?; + let account_c = account_address("c")?; + let account_d = account_address("d")?; + + let a_airdrop = Uint128(50000000); + let b_airdrop = Uint128(20000000); + let ab_half_airdrop = Uint128(35000000); + let c_airdrop = Uint128(10000000); + let total_airdrop = a_airdrop + b_airdrop + c_airdrop; // 80000000 + let decay_amount = Uint128(10000000); + + /// Initialize dummy snip20 + print_header("\nInitializing snip20"); + + let snip_init_msg = snip20::InitMsg { + name: "test".to_string(), + admin: None, + symbol: "TEST".to_string(), + decimals: 6, + initial_balances: Some(vec![InitialBalance { + address: HumanAddr::from(account_a.clone()), + amount: total_airdrop + decay_amount, + }]), + prng_seed: Default::default(), + config: Some(InitConfig { + public_total_supply: Some(true), + enable_deposit: Some(true), + enable_redeem: Some(true), + enable_mint: Some(true), + enable_burn: Some(false), + }), + }; + + let snip = test_inst_init( + &snip_init_msg, + SNIP20_FILE, + &*generate_label(8), + ACCOUNT_KEY, + Some(STORE_GAS), + Some(GAS), + Some("test"), + )?; + print_contract(&snip); + + { + let msg = snip20::HandleMsg::SetViewingKey { + key: String::from(VIEW_KEY), + padding: None, + }; + + test_contract_handle(&msg, &snip, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + print_header("Creating merkle tree"); + let leaves: Vec<[u8; 32]> = vec![ + Sha256::hash((account_a.clone() + &a_airdrop.to_string()).as_bytes()), + Sha256::hash((account_b.clone() + &b_airdrop.to_string()).as_bytes()), + Sha256::hash((account_c.clone() + &c_airdrop.to_string()).as_bytes()), + Sha256::hash((account_d.clone() + &decay_amount.to_string()).as_bytes()), + ]; + + let merlke_tree = MerkleTree::::from_leaves(&leaves); + + print_header("Initializing airdrop"); + + let now = chrono::offset::Utc::now().timestamp() as u64; + let duration = 200; + let decay_date = now + duration; + let end_date = decay_date + 60; + + let airdrop_init_msg = airdrop::InitMsg { + admin: None, + dump_address: Some(HumanAddr::from(account_a.clone())), + airdrop_token: Contract { + address: HumanAddr::from(snip.address.clone()), + code_hash: snip.code_hash.clone(), + }, + airdrop_amount: total_airdrop + decay_amount, + start_date: None, + end_date: Some(end_date), + decay_start: Some(decay_date), + merkle_root: Binary(merlke_tree.root().unwrap().to_vec()), + total_accounts: leaves.len() as u32, + max_amount: a_airdrop, + default_claim: Uint128(50), + task_claim: vec![RequiredTask { + address: HumanAddr::from(account_a.clone()), + percent: Uint128(50), + }], + query_rounding: Uint128(30000000), + }; + + let airdrop = test_inst_init( + &airdrop_init_msg, + AIRDROP_FILE, + &*generate_label(8), + ACCOUNT_KEY, + Some(STORE_GAS), + Some(GAS), + Some("test"), + )?; + + print_contract(&airdrop); + + /// Assert that we start with nothing + test_contract_handle( + &snip20::HandleMsg::Send { + recipient: HumanAddr::from(airdrop.address.clone()), + amount: total_airdrop + decay_amount, + msg: None, + memo: None, + padding: None, + }, + &snip, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + assert_eq!(Uint128(0), get_balance(&snip, account_a.clone())); + + print_warning("Creating initial permits"); + /// Create AB permit + let b_address_proof = AddressProofMsg { + address: HumanAddr(account_b.clone()), + amount: b_airdrop, + contract: HumanAddr(airdrop.address.clone()), + index: 1, + key: "key".to_string(), + }; + + let b_permit = create_signed_permit(b_address_proof, "b"); + + let a_address_proof = AddressProofMsg { + address: HumanAddr(account_a.clone()), + amount: a_airdrop, + contract: HumanAddr(airdrop.address.clone()), + index: 0, + key: "key".to_string(), + }; + + print_warning("Creating proof"); + let initial_proof = proof_from_tree(&vec![0, 1], &merlke_tree.layers()); + + let a_permit = create_signed_permit(a_address_proof, ACCOUNT_KEY); + let account_permit = create_signed_permit( + AccountPermitMsg { + contract: HumanAddr(airdrop.address.clone()), + key: "key".to_string(), + }, + ACCOUNT_KEY, + ); + + /// Create an account which will also claim whatever amount is available + print_warning("Creating an account"); + { + let tx_info = test_contract_handle( + &airdrop::HandleMsg::CreateAccount { + addresses: vec![b_permit, a_permit.clone()], + partial_tree: initial_proof, + padding: None, + }, + &airdrop, + ACCOUNT_KEY, + Some("1000000"), + Some("test"), + None, + )? + .1; + + println!("Gas used: {}", tx_info.gas_used); + } + + print_warning("Getting initial account information"); + { + let msg = airdrop::QueryMsg::Account { + permit: account_permit.clone(), + current_date: None, + }; + + let query: airdrop::QueryAnswer = query_contract(&airdrop, msg)?; + + if let airdrop::QueryAnswer::Account { + total, + claimed, + unclaimed, + finished_tasks, + } = query + { + assert_eq!(total, a_airdrop + b_airdrop); + assert_eq!(claimed, ab_half_airdrop); + assert_eq!(unclaimed, Uint128::zero()); + assert_eq!(finished_tasks.len(), 1); + } + } + + /// Assert that we claimed + assert_eq!(ab_half_airdrop, get_balance(&snip, account_a.clone())); + + print_warning("Enabling the other half of the airdrop"); + + test_contract_handle( + &airdrop::HandleMsg::CompleteTask { + address: HumanAddr::from(account_a.clone()), + padding: None, + }, + &airdrop, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + { + let msg = airdrop::QueryMsg::Account { + permit: account_permit.clone(), + current_date: None, + }; + + let query: airdrop::QueryAnswer = query_contract(&airdrop, msg)?; + + if let airdrop::QueryAnswer::Account { + total, + claimed, + unclaimed, + finished_tasks, + } = query + { + assert_eq!(total, a_airdrop + b_airdrop); + assert_eq!(claimed, ab_half_airdrop); + assert_eq!(unclaimed, ab_half_airdrop); + assert_eq!(finished_tasks.len(), 2); + } + } + + print_warning("Verifying query step functionality"); + + { + let msg = airdrop::QueryMsg::TotalClaimed {}; + + let query: airdrop::QueryAnswer = query_contract(&airdrop, msg)?; + + if let airdrop::QueryAnswer::TotalClaimed { claimed } = query { + assert_eq!(claimed, Uint128(30000000)); + } + } + + print_warning("Confirming full airdrop after adding C"); + + let c_address_proof = AddressProofMsg { + address: HumanAddr(account_c.clone()), + amount: c_airdrop, + contract: HumanAddr(airdrop.address.clone()), + index: 2, + key: "key".to_string(), + }; + + let c_permit = create_signed_permit(c_address_proof, "c"); + let other_proof = proof_from_tree(&vec![2], &merlke_tree.layers()); + + test_contract_handle( + &airdrop::HandleMsg::UpdateAccount { + addresses: vec![c_permit], + partial_tree: other_proof, + padding: None, + }, + &airdrop, + ACCOUNT_KEY, + Some("1000000"), + Some("test"), + None, + )?; + + /// Assert that we claimed + assert_eq!(total_airdrop, get_balance(&snip, account_a.clone())); + + /// Query that all of the airdrop is claimed + { + let msg = airdrop::QueryMsg::Account { + permit: account_permit.clone(), + current_date: None, + }; + + let query: airdrop::QueryAnswer = query_contract(&airdrop, msg)?; + + if let airdrop::QueryAnswer::Account { + total, + claimed, + unclaimed, + finished_tasks, + } = query + { + assert_eq!(total, total_airdrop); + assert_eq!(claimed, total_airdrop); + assert_eq!(unclaimed, Uint128::zero()); + assert_eq!(finished_tasks.len(), 2); + } + } + + print_warning("Disabling permit"); + test_contract_handle( + &airdrop::HandleMsg::DisablePermitKey { + key: "key".to_string(), + padding: None, + }, + &airdrop, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + { + let msg = airdrop::QueryMsg::Account { + permit: account_permit.clone(), + current_date: None, + }; + + let query: Result = query_contract(&airdrop, msg); + + assert!(query.is_err()); + } + + let new_account_permit = create_signed_permit( + AccountPermitMsg { + contract: HumanAddr(airdrop.address.clone()), + key: "new_key".to_string(), + }, + ACCOUNT_KEY, + ); + + { + let msg = airdrop::QueryMsg::Account { + permit: new_account_permit.clone(), + current_date: None, + }; + + let query: airdrop::QueryAnswer = query_contract(&airdrop, msg)?; + + if let airdrop::QueryAnswer::Account { + total, + claimed, + unclaimed, + finished_tasks, + } = query + { + assert_eq!(total, total_airdrop); + assert_eq!(claimed, total_airdrop); + assert_eq!(unclaimed, Uint128::zero()); + assert_eq!(finished_tasks.len(), 2); + } + } + + print_warning("Claiming partially decayed tokens"); + { + let current = chrono::offset::Utc::now().timestamp() as u64; + // Wait until times is between decay start and end of airdrop + thread::sleep(time::Duration::from_secs((decay_date - current) + 20)); + } + + { + let d_address_proof = AddressProofMsg { + address: HumanAddr(account_d.clone()), + amount: decay_amount, + contract: HumanAddr(airdrop.address.clone()), + index: 3, + key: "key".to_string(), + }; + + let d_permit = create_signed_permit(d_address_proof, "d"); + let d_proof = proof_from_tree(&vec![3], &merlke_tree.layers()); + + test_contract_handle( + &airdrop::HandleMsg::UpdateAccount { + addresses: vec![d_permit], + partial_tree: d_proof, + padding: None, + }, + &airdrop, + ACCOUNT_KEY, + Some("1000000"), + Some("test"), + None, + )?; + + let balance = get_balance(&snip, account_a.clone()); + + assert!(balance > total_airdrop); + assert!(balance < total_airdrop + decay_amount); + } + + let pre_decay_balance = get_balance(&snip, account_a.clone()); + + /// Try to claim expired tokens + print_warning("Claiming expired tokens"); + { + let current = chrono::offset::Utc::now().timestamp() as u64; + // Wait until times is between decay start and end of airdrop + thread::sleep(time::Duration::from_secs(end_date - current + 20)); + } + + test_contract_handle( + &airdrop::HandleMsg::ClaimDecay { + padding: None, + }, + &airdrop, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + assert_eq!( + total_airdrop + decay_amount, + get_balance(&snip, account_a.clone()) + ); + + print_warning("Verifying query step functionality after decay claim"); + + { + let msg = airdrop::QueryMsg::TotalClaimed {}; + + let query: airdrop::QueryAnswer = query_contract(&airdrop, msg)?; + + if let airdrop::QueryAnswer::TotalClaimed { claimed } = query { + assert_eq!(claimed, pre_decay_balance); + } + } + + Ok(()) +} + +#[test] +fn run_testnet() -> Result<()> { + let account = account_address(ACCOUNT_KEY)?; + + println!("Using Account: {}", account.blue()); + + /// Initialize sSCRT + print_header("Initializing sSCRT"); + + let sscrt_init_msg = snip20::InitMsg { + name: "sSCRT".to_string(), + admin: None, + symbol: "SSCRT".to_string(), + decimals: 6, + initial_balances: None, + prng_seed: Default::default(), + config: Some(InitConfig { + public_total_supply: Some(true), + enable_deposit: Some(true), + enable_redeem: Some(true), + enable_mint: Some(true), + enable_burn: Some(false), + }), + }; + + let s_sCRT = test_inst_init( + &sscrt_init_msg, + SNIP20_FILE, + &*generate_label(8), + ACCOUNT_KEY, + Some(STORE_GAS), + Some(GAS), + Some("test"), + )?; + print_contract(&s_sCRT); + + { + let msg = snip20::HandleMsg::SetViewingKey { + key: String::from(VIEW_KEY), + padding: None, + }; + + test_contract_handle(&msg, &s_sCRT, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + println!("\n\tDepositing 1000000000uscrt"); + + { + let msg = snip20::HandleMsg::Deposit { padding: None }; + + test_contract_handle( + &msg, + &s_sCRT, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + Some("1000000000uscrt"), + )?; + } + + // Initialize initializer and snip20s + let (initializer, shade, silk) = + initialize_initializer(account.clone(), &s_sCRT, account.clone())?; + + // Initialize Governance + print_header("Initializing Governance"); + + let governance_init_msg = governance::InitMsg { + admin: None, + // The next governance votes will not require voting + staker: None, + funding_token: Contract { + address: HumanAddr::from(shade.address.clone()), + code_hash: shade.code_hash.clone(), + }, + funding_amount: Uint128(1000000), + funding_deadline: 180, + voting_deadline: 180, + // 5 shade is the minimum + quorum: Uint128(5000000), + }; + + let governance = test_inst_init( + &governance_init_msg, + GOVERNANCE_FILE, + &*generate_label(8), + ACCOUNT_KEY, + Some(STORE_GAS), + Some(GAS), + Some("test"), + )?; + + print_contract(&governance); + + // Add contracts + add_contract("initializer".to_string(), &initializer, &governance)?; + add_contract("shade".to_string(), &shade, &governance)?; + add_contract("silk".to_string(), &silk, &governance)?; + + // Change contract admin + { + let msg = snip20::HandleMsg::ChangeAdmin { + address: HumanAddr::from(governance.address.clone()), + padding: None, + }; + + test_contract_handle(&msg, &shade, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + test_contract_handle(&msg, &silk, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + + // Print Contracts so far + print_warning("Governance contracts so far"); + { + let msg = governance::QueryMsg::GetSupportedContracts {}; + + let query: governance::QueryAnswer = query_contract(&governance, &msg)?; + + if let governance::QueryAnswer::SupportedContracts { contracts } = query { + print_vec("Contracts: ", contracts); + } + } + // Set Snip20s + print_warning("Getting Shade contract from governance"); + let shade_contract = get_contract(&governance, "shade".to_string())?; + print_warning("Getting Silk contract from governance"); + let silk_contract = get_contract(&governance, "silk".to_string())?; + + // Initialize Band Mock + let band = init_contract( + &governance, + "band_mock".to_string(), + MOCK_BAND_FILE, + band::InitMsg {}, + )?; + + // Initialize Oracle + let oracle = init_contract( + &governance, + "oracle".to_string(), + ORACLE_FILE, + oracle::InitMsg { + admin: None, + band: Contract { + address: HumanAddr::from(band.address), + code_hash: band.code_hash, + }, + sscrt: Contract { + address: HumanAddr::from(s_sCRT.address.clone()), + code_hash: s_sCRT.code_hash.clone(), + }, + }, + )?; + + // Initialize Mint-Shade + let mint_shade = initialize_minter(&governance, "shade_minter".to_string(), &shade_contract)?; + + // Initialize Mint-Silk + let mint_silk = initialize_minter(&governance, "silk_minter".to_string(), &silk_contract)?; + + // Setup mint contracts + // This also tests that governance can update allowed contracts + setup_minters( + &governance, + &mint_shade, + &mint_silk, + &shade_contract, + &silk_contract, + &s_sCRT, + )?; + + // Initialize staking + let staker = setup_staker(&governance, &shade_contract, account.clone())?; + + // Set governance to require voting + print_warning("Enabling governance voting"); + create_and_trigger_proposal( + &governance, + governance::GOVERNANCE_SELF.to_string(), + governance::HandleMsg::UpdateConfig { + admin: None, + staker: Some(Contract { + address: HumanAddr::from(staker.address.clone()), + code_hash: staker.code_hash.clone(), + }), + proposal_deadline: None, + funding_amount: None, + funding_deadline: None, + minimum_votes: None, + }, + Some("Remove control from admin and initialize governance"), + )?; + + // Proposal admin command + print_header("Creating proposal expected to fail"); + let admin_command = "{\"update_config\":{\"unbond_time\": {}, \"admin\": null}}"; + + // Create a proposal and vote half of the votes + create_proposal( + &governance, + governance::GOVERNANCE_SELF.to_string(), + governance::HandleMsg::AddAdminCommand { + name: "stake_unbond_time".to_string(), + proposal: admin_command.to_string(), + }, + Some("Staker unbond time can be updated by admin whenever"), + )?; + + // Fund the proposal + print_warning("Funding proposal"); + { + let proposal = get_latest_proposal(&governance)?; + + // Check that its in a funding period + { + let msg = governance::QueryMsg::GetProposal { + proposal_id: proposal, + }; + + let query: governance::QueryAnswer = query_contract(&governance, msg)?; + + if let governance::QueryAnswer::Proposal { proposal } = query { + assert_eq!(proposal.status, ProposalStatus::Funding); + } else { + assert!(false, "Query returned unexpected response") + } + } + + let balance = get_balance(&shade, account.clone()); + + test_contract_handle( + &snip20::HandleMsg::Send { + recipient: HumanAddr::from(governance.address.clone()), + amount: Uint128(1000000), + msg: Some(to_binary(&proposal).unwrap()), + memo: None, + padding: None, + }, + &shade, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + // Check that its in a voting period + { + let msg = governance::QueryMsg::GetProposal { + proposal_id: proposal, + }; + + let query: governance::QueryAnswer = query_contract(&governance, msg)?; + + if let governance::QueryAnswer::Proposal { proposal } = query { + assert_eq!(proposal.status, ProposalStatus::Voting); + } else { + assert!(false, "Query returned unexpected response") + } + } + + print_warning("Checking that funds are returned"); + assert_eq!(balance, get_balance(&shade, account.clone())); + } + + print_warning("Voting on proposal"); + { + let proposal_time = chrono::offset::Utc::now().timestamp() as u64; + let proposal = get_latest_proposal(&governance)?; + + // Vote on proposal + test_contract_handle( + &staking::HandleMsg::Vote { + proposal_id: proposal, + votes: vec![UserVote { + vote: Vote::Yes, + weight: 50, + }], + }, + &staker, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + // Verify that the proposal votes were properly done + { + let msg = governance::QueryMsg::GetProposalVotes { + proposal_id: proposal, + }; + + let query: governance::QueryAnswer = query_contract(&governance, msg)?; + + if let governance::QueryAnswer::ProposalVotes { status } = query { + assert_eq!(status.abstain, Uint128(0)); + assert_eq!(status.no, Uint128(0)); + assert_eq!(status.yes, Uint128(2500000)); + } else { + assert!(false, "Query returned unexpected response") + } + } + + // Try to trigger the proposal before the time limit is reached + trigger_latest_proposal(&governance)?; + + // Query its status + { + let msg = governance::QueryMsg::GetProposal { + proposal_id: proposal, + }; + + let query: governance::QueryAnswer = query_contract(&governance, msg)?; + + if let governance::QueryAnswer::Proposal { proposal } = query { + assert_eq!(proposal.status, ProposalStatus::Voting); + } else { + assert!(false, "Query returned unexpected response") + } + } + + // Wait for the time limit and try to trigger it + let now = chrono::offset::Utc::now().timestamp() as u64; + thread::sleep(time::Duration::from_secs(proposal_time + 184 - now)); + + trigger_latest_proposal(&governance)?; + + // Query its status and expect it to break + { + let msg = governance::QueryMsg::GetProposal { + proposal_id: proposal, + }; + + let query: governance::QueryAnswer = query_contract(&governance, msg)?; + + if let governance::QueryAnswer::Proposal { proposal } = query { + assert_eq!(proposal.status, ProposalStatus::Expired); + } else { + assert!(false, "Query returned unexpected response") + } + } + } + + print_header("Creating admin command"); + create_proposal( + &governance, + governance::GOVERNANCE_SELF.to_string(), + governance::HandleMsg::AddAdminCommand { + name: "stake_unbond_time".to_string(), + proposal: admin_command.to_string(), + }, + Some("Staker unbond time can be updated by admin whenever"), + )?; + + { + let proposal_time = chrono::offset::Utc::now().timestamp() as u64; + let proposal = get_latest_proposal(&governance)?; + + print_warning("Funding proposal"); + test_contract_handle( + &snip20::HandleMsg::Send { + recipient: HumanAddr::from(governance.address.clone()), + amount: Uint128(1000000), + msg: Some(to_binary(&proposal).unwrap()), + memo: None, + padding: None, + }, + &shade, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + // Vote on proposal + print_warning("Voting on proposal"); + test_contract_handle( + &staking::HandleMsg::Vote { + proposal_id: proposal, + votes: vec![ + UserVote { + vote: Vote::Yes, + weight: 50, + }, + UserVote { + vote: Vote::No, + weight: 25, + }, + UserVote { + vote: Vote::Abstain, + weight: 25, + }, + ], + }, + &staker, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + // Wait for the time limit and try to trigger it + let now = chrono::offset::Utc::now().timestamp() as u64; + thread::sleep(time::Duration::from_secs(proposal_time + 184 - now)); + trigger_latest_proposal(&governance)?; + + // Query its status and expect it to finish + { + let msg = governance::QueryMsg::GetProposal { + proposal_id: proposal, + }; + + let query: governance::QueryAnswer = query_contract(&governance, msg)?; + + if let governance::QueryAnswer::Proposal { proposal } = query { + assert_eq!(proposal.status, ProposalStatus::Passed); + assert_eq!(proposal.run_status, Some(ResponseStatus::Success)); + } else { + assert!(false, "Query returned unexpected response") + } + } + } + + // Trigger the admin command + print_warning("Triggering admin command"); + test_contract_handle( + &governance::HandleMsg::TriggerAdminCommand { + target: "staking".to_string(), + command: "stake_unbond_time".to_string(), + variables: vec!["240".to_string()], + description: "Extending unbond time for staking contract".to_string(), + }, + &governance, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + // Check that admin command did something + { + let msg = staking::QueryMsg::Config {}; + + let query: staking::QueryAnswer = query_contract(&staker, msg)?; + + if let staking::QueryAnswer::Config { config } = query { + assert_eq!(config.unbond_time, 240); + } else { + assert!(false, "Query returned unexpected response") + } + } + + // Make a failed funding period + print_header("Testing failed funding"); + create_proposal( + &governance, + governance::GOVERNANCE_SELF.to_string(), + governance::HandleMsg::AddAdminCommand { + name: "stake_unbond_time".to_string(), + proposal: admin_command.to_string(), + }, + Some("This wont be funded :("), + )?; + + { + print_warning("Trying to fund"); + let proposal = get_latest_proposal(&governance)?; + let proposal_time = chrono::offset::Utc::now().timestamp() as u64; + + let lost_amount = Uint128(500000); + let balance_before = get_balance(&shade, account.clone()); + + test_contract_handle( + &snip20::HandleMsg::Send { + recipient: HumanAddr::from(governance.address.clone()), + amount: lost_amount, + msg: Some(to_binary(&proposal).unwrap()), + memo: None, + padding: None, + }, + &shade, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + let balance_after = get_balance(&shade, account.clone()); + + assert_ne!(balance_before, balance_after); + + // Wait funding period + let now = chrono::offset::Utc::now().timestamp() as u64; + thread::sleep(time::Duration::from_secs(proposal_time + 184 - now)); + + // Trigger funding + test_contract_handle( + &snip20::HandleMsg::Send { + recipient: HumanAddr::from(governance.address.clone()), + amount: lost_amount, + msg: Some(to_binary(&proposal).unwrap()), + memo: None, + padding: None, + }, + &shade, + ACCOUNT_KEY, + Some(GAS), + Some("test"), + None, + )?; + + assert_eq!(get_balance(&shade, account.clone()), balance_after); + + print_warning("Proposal must be expired"); + { + let msg = governance::QueryMsg::GetProposal { + proposal_id: proposal, + }; + + let query: governance::QueryAnswer = query_contract(&governance, msg)?; + + if let governance::QueryAnswer::Proposal { proposal } = query { + assert_eq!(proposal.status, ProposalStatus::Expired); + } else { + assert!(false, "Query returned unexpected response") + } + } + } + + Ok(()) +} diff --git a/packages/secretcli/Cargo.toml b/packages/secretcli/Cargo.toml new file mode 100644 index 000000000..bc80738af --- /dev/null +++ b/packages/secretcli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "secretcli" +version = "0.1.0" +authors = ["Guy Garcia "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] + +[dependencies] +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.67"} diff --git a/packages/secretcli/src/cli_types.rs b/packages/secretcli/src/cli_types.rs new file mode 100644 index 000000000..ea2b46a3d --- /dev/null +++ b/packages/secretcli/src/cli_types.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct TxResponse { + pub height: String, + pub txhash: String, + pub codespace: String, + pub code: Option, + pub data: String, + pub raw_log: String, +} + +#[derive(Serialize, Deserialize)] +pub struct TxCompute { + //#[serde(rename="key")] + //pub msg_key: String, + pub input: String, +} + +#[derive(Serialize, Deserialize)] +pub struct TxQuery { + pub height: String, + pub txhash: String, + pub data: String, + pub raw_log: String, + pub logs: Vec, + pub gas_wanted: String, + pub gas_used: String, + //pub tx: String, + pub timestamp: String, +} + +#[derive(Serialize, Deserialize)] +pub struct TxQueryLogs { + pub msg_index: i128, + pub log: String, + pub events: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct TxQueryEvents { + #[serde(rename = "type")] + pub msg_type: String, + pub attributes: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct TxQueryKeyValue { + #[serde(rename = "key")] + pub msg_key: String, + pub value: String, +} + +#[derive(Serialize, Deserialize)] +pub struct ListCodeResponse { + pub id: u128, + pub creator: String, + pub data_hash: String, +} + +#[derive(Serialize, Deserialize)] +pub struct ListContractCode { + pub code_id: u128, + pub creator: String, + pub label: String, + pub address: String, +} + +#[derive(Serialize, Deserialize)] +pub struct NetContract { + pub label: String, + pub id: String, + pub address: String, + pub code_hash: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SignedTx { + pub pub_key: PubKey, + pub signature: String, +} + +#[derive(Serialize, Deserialize)] +pub struct PubKey { + #[serde(rename = "type")] + pub msg_type: String, + pub value: String, +} diff --git a/packages/secretcli/src/lib.rs b/packages/secretcli/src/lib.rs new file mode 100644 index 000000000..98dd50ad7 --- /dev/null +++ b/packages/secretcli/src/lib.rs @@ -0,0 +1,2 @@ +pub mod cli_types; +pub mod secretcli; diff --git a/packages/secretcli/src/secretcli.rs b/packages/secretcli/src/secretcli.rs new file mode 100644 index 000000000..652008f95 --- /dev/null +++ b/packages/secretcli/src/secretcli.rs @@ -0,0 +1,515 @@ +use crate::cli_types::{ + ListCodeResponse, + ListContractCode, + NetContract, + SignedTx, + TxCompute, + TxQuery, + TxResponse, +}; +use serde_json::{Result, Value}; +use std::{fs::File, io::Write, process::Command, thread, time}; + +//secretcli tx sign-doc tx_to_sign --from sign-test + +fn vec_str_to_vec_string(str_in: Vec<&str>) -> Vec { + let mut str_out: Vec = vec![]; + + for val in str_in { + str_out.push(val.to_string()); + } + + str_out +} + +/// +/// Will run any scretcli command and return its output +/// +/// # Arguments +/// +/// * 'command' - a string array that contains the command to forward\ +/// +pub fn secretcli_run(command: Vec) -> Result { + let retry = 20; + let mut commands = command; + commands.append(&mut vec_str_to_vec_string(vec!["--output", "json"])); + let mut cli = Command::new("secretd".to_string()); + if !commands.is_empty() { + cli.args(commands); + } + + let mut result = cli.output().expect("Unexpected error"); + + // We wait cause sometimes the query/action takes a while + for _ in 0..retry { + if !result.stderr.is_empty() { + thread::sleep(time::Duration::from_secs(1)); + } else { + break; + } + result = cli.output().expect("Unexpected error"); + } + let out = result.stdout; + serde_json::from_str(&String::from_utf8_lossy(&out)) +} + +/// +/// Stores the given contract +/// +/// # Arguments +/// +/// * 'contract' - Contract to be stored +/// * 'user' - User that will handle the tx, defaults to a +/// * 'gas' - Gas to pay, defaults to 10000000 +/// * 'backend' - The backend keyring, defaults to test +/// +pub fn store_contract( + contract: &str, + user: Option<&str>, + gas: Option<&str>, + backend: Option<&str>, +) -> Result { + let mut command_arr = vec![ + "tx", + "compute", + "store", + contract, + "--from", + user.unwrap_or("a"), + "--gas", + gas.unwrap_or("10000000"), + "-y", + ]; + + if let Some(backend) = backend { + command_arr.push("--keyring-backend"); + command_arr.push(backend); + } + + let command = vec_str_to_vec_string(command_arr); + let json = secretcli_run(command)?; + let out: Result = serde_json::from_value(json); + out +} + +/// +/// Queries the hash information +/// +pub fn query_hash(hash: String) -> Result { + let command = vec!["q", "tx", &hash]; + let a = secretcli_run(vec_str_to_vec_string(command))?; + + serde_json::from_value(a) +} + +/// +/// Computes the hash information +/// +pub fn compute_hash(hash: String) -> Result { + let command = vec!["q", "compute", "tx", &hash]; + + serde_json::from_value(secretcli_run(vec_str_to_vec_string(command))?) +} + +/// +/// Lists all uploaded contracts +/// +pub fn list_code() -> Result> { + let command = vec!["query", "compute", "list-code"]; + + serde_json::from_value(secretcli_run(vec_str_to_vec_string(command))?) +} + +pub fn list_contracts_by_code(code: String) -> Result> { + let command = vec!["query", "compute", "list-contract-by-code", &code]; + + serde_json::from_value(secretcli_run(vec_str_to_vec_string(command))?) +} + +fn trim_newline(s: &mut String) { + if s.ends_with('\n') { + s.pop(); + if s.ends_with('\r') { + s.pop(); + } + } +} + +pub fn account_address(acc: &str) -> Result { + let command = vec_str_to_vec_string(vec!["keys", "show", "-a", acc]); + + let retry = 20; + let mut cli = Command::new("secretd".to_string()); + if !command.is_empty() { + cli.args(command); + } + + let mut result = cli.output().expect("Unexpected error"); + + // We wait cause sometimes the query/action takes a while + for _ in 0..retry { + if !result.stderr.is_empty() { + thread::sleep(time::Duration::from_secs(1)); + } else { + break; + } + result = cli.output().expect("Unexpected error"); + } + + let out = result.stdout; + + let mut s: String = String::from_utf8_lossy(&out).parse().unwrap(); + + // Sometimes the resulting string has a newline, so we trim that + trim_newline(&mut s); + + Ok(s) +} + +/// +/// Instantiate a contract +/// +/// # Arguments +/// +/// * 'code_id' - The contract to interact with +/// * 'msg' - The init msg to serialize +/// * 'label' - The contract label +/// * 'sender' - Msg sender +/// * 'gas' - Gas price to use, defaults to 8000000 +/// * 'backend' - Keyring backend defaults to none +/// +pub fn instantiate_contract( + contract: &NetContract, + msg: Init, + label: &str, + sender: &str, + gas: Option<&str>, + backend: Option<&str>, +) -> Result { + let message = serde_json::to_string(&msg)?; + + let mut command = vec![ + "tx", + "compute", + "instantiate", + &contract.id, + &message, + "--from", + sender, + "--label", + label, + "--gas", + ]; + + command.push(match gas { + None => "10000000", + Some(gas) => gas, + }); + + if let Some(backend) = backend { + command.push("--keyring-backend"); + command.push(backend); + } + + command.push("-y"); + + let response: TxResponse = + serde_json::from_value(secretcli_run(vec_str_to_vec_string(command))?)?; + + Ok(response) +} + +/// +/// Trait that allows contract init to be used in test scripts +/// +/// # Arguments +/// +/// * 'contract' - The contract to interact with +/// * 'label' - The contract label +/// * 'sender' - Msg sender +/// * 'gas' - Gas price to use, defaults to 8000000 +/// * 'backend' - Keyring backend defaults to none +/// +pub trait TestInit: serde::Serialize { + fn t_init( + &self, + contract: &NetContract, + label: &str, + sender: &str, + gas: Option<&str>, + backend: Option<&str>, + ) -> Result { + let tx = instantiate_contract(contract, self, label, sender, gas, backend)?; + query_hash(tx.txhash) + } + + fn inst_init( + &self, + contract_file: &str, + label: &str, + sender: &str, + store_gas: Option<&str>, + init_gas: Option<&str>, + backend: Option<&str>, + ) -> Result { + let store_response = + store_contract(contract_file, Option::from(&*sender), store_gas, backend)?; + + let store_query = query_hash(store_response.txhash)?; + + let mut contract = NetContract { + label: label.to_string(), + id: "".to_string(), + address: "".to_string(), + code_hash: "".to_string(), + }; + + // Look for the code ID + for attribute in &store_query.logs[0].events[0].attributes { + if attribute.msg_key == "code_id" { + contract.id = attribute.value.clone(); + break; + } + } + + let init_query = self.t_init(&contract, label, sender, init_gas, backend)?; + + // Look for the contract's address + for attribute in &init_query.logs[0].events[0].attributes { + if attribute.msg_key == "contract_address" { + contract.address = attribute.value.clone(); + break; + } + } + + // Look for the contract's code hash + let listed_contracts = list_code()?; + + for item in listed_contracts { + if item.id.to_string() == contract.id { + contract.code_hash = item.data_hash; + break; + } + } + + Ok(contract) + } +} + +pub fn test_init( + msg: &Message, + contract: &NetContract, + label: &str, + sender: &str, + gas: Option<&str>, + backend: Option<&str>, +) -> Result { + let tx = instantiate_contract(contract, msg, label, sender, gas, backend)?; + query_hash(tx.txhash) +} + +pub fn test_inst_init( + msg: &Message, + contract_file: &str, + label: &str, + sender: &str, + store_gas: Option<&str>, + init_gas: Option<&str>, + backend: Option<&str>, +) -> Result { + let store_response = store_contract(contract_file, Option::from(&*sender), store_gas, backend)?; + let store_query = query_hash(store_response.txhash)?; + let mut contract = NetContract { + label: label.to_string(), + id: "".to_string(), + address: "".to_string(), + code_hash: "".to_string(), + }; + // Look for the code ID + for attribute in &store_query.logs[0].events[0].attributes { + if attribute.msg_key == "code_id" { + contract.id = attribute.value.clone(); + break; + } + } + let init_query = test_init(&msg, &contract, label, sender, init_gas, backend)?; + + // Look for the contract's address + for attribute in &init_query.logs[0].events[0].attributes { + if attribute.msg_key == "contract_address" { + contract.address = attribute.value.clone(); + break; + } + } + // Look for the contract's code hash + let listed_contracts = list_code()?; + + for item in listed_contracts { + if item.id.to_string() == contract.id { + contract.code_hash = item.data_hash; + break; + } + } + Ok(contract) +} + +/// +/// Executes a contract's handle +/// +/// # Arguments +/// +/// * 'contract' - The contract to interact with +/// * 'msg' - The handle msg to serialize +/// * 'sender' - Msg sender +/// * 'gas' - Gas price to use, defaults to 8000000 +/// * 'backend' - Keyring backend defaults to none +/// * 'amount' - Included L1 tokens to send, defaults to none +/// +pub fn execute_contract( + contract: &NetContract, + msg: Handle, + sender: &str, + gas: Option<&str>, + backend: Option<&str>, + amount: Option<&str>, +) -> Result { + let message = serde_json::to_string(&msg)?; + + let mut command = vec![ + "tx", + "compute", + "execute", + &contract.address, + &message, + "--from", + sender, + "--gas", + ]; + + command.push(match gas { + None => "800000", + Some(gas) => gas, + }); + + if let Some(backend) = backend { + command.push("--keyring-backend"); + command.push(backend); + } + + if let Some(amount) = amount { + command.push("--amount"); + command.push(amount); + } + + command.push("-y"); + + let response: TxResponse = + serde_json::from_value(secretcli_run(vec_str_to_vec_string(command))?)?; + + Ok(response) +} + +/// +/// Trait that allows contract handle enums to be used in test scripts +/// +/// # Arguments +/// +/// * 'contract' - The contract to interact with +/// * 'sender' - Msg sender +/// * 'gas' - Gas price to use, defaults to 8000000 +/// * 'backend' - Keyring backend defaults to none +/// * 'amount' - Included L1 tokens to send, defaults to none +/// +pub trait TestHandle: serde::Serialize { + fn t_handle( + &self, + contract: &NetContract, + sender: &str, + gas: Option<&str>, + backend: Option<&str>, + amount: Option<&str>, + ) -> Result { + let tx = execute_contract(contract, self, sender, gas, backend, amount)?; + + let response: Result = compute_hash(tx.txhash); + response + } +} + +/// +/// Function equivalent of the TestHandle trait +/// +pub fn test_contract_handle( + msg: &Message, + contract: &NetContract, + sender: &str, + gas: Option<&str>, + backend: Option<&str>, + amount: Option<&str>, +) -> Result<(TxCompute, TxQuery)> { + let tx = execute_contract(contract, msg, sender, gas, backend, amount)?; + + let computed_response = compute_hash(tx.txhash.clone())?; + let queried_response = query_hash(tx.txhash)?; + Ok((computed_response, queried_response)) +} + +/// +/// Queries a given contract +/// +/// # Arguments +/// +/// * 'contract' - The contract to query +/// * 'msg' - The query to serialize, must have serde::Serialized +/// +pub fn query_contract( + contract: &NetContract, + msg: Query, +) -> Result { + let command = vec_str_to_vec_string(vec![ + "query", + "compute", + "query", + &contract.address, + &serde_json::to_string(&msg)?, + ]); + + let response: Result = serde_json::from_value(secretcli_run(command)?); + response +} + +/// +/// Trait that allows contract query enums to be used in test scripts +/// +/// # Arguments +/// +/// * 'contract' - The contract to query +/// +pub trait TestQuery: serde::Serialize { + fn t_query(&self, contract: &NetContract) -> Result { + query_contract(contract, self) + } +} + +/// +/// Create a signed permit +/// +/// # Arguments +/// +/// * 'tx' - The message to sign +/// * 'signer' - The key of the signer +/// +pub fn create_permit(tx: Tx, signer: &str) -> Result { + let msg = serde_json::to_string(&tx)?; + + // send to a file + let mut file = File::create("./tx_to_sign").unwrap(); + file.write_all(msg.as_bytes()).unwrap(); + + let command = vec!["tx", "sign-doc", "tx_to_sign", "--from", signer]; + + let response: SignedTx = + serde_json::from_value(secretcli_run(vec_str_to_vec_string(command))?)?; + + Ok(response) +} diff --git a/packages/shade_protocol/Cargo.toml b/packages/shade_protocol/Cargo.toml index 12d5f1d59..a22c994d8 100644 --- a/packages/shade_protocol/Cargo.toml +++ b/packages/shade_protocol/Cargo.toml @@ -10,18 +10,6 @@ edition = "2018" [lib] crate-type = ["cdylib", "rlib"] - -[profile.release] -opt-level = 3 -debug = false -rpath = false -lto = true -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false -overflow-checks = true - [features] default = [] # for quicker tests, cargo test --lib @@ -30,11 +18,18 @@ backtraces = ["cosmwasm-std/backtraces"] debug-print = ["cosmwasm-std/debug-print"] [dependencies] -cosmwasm-schema = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-std = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -cosmwasm-storage = { git = "https://github.com/enigmampc/SecretNetwork", tag = "v1.0.4-debug-print" } -secret-toolkit = { git = "https://github.com/enigmampc/secret-toolkit", branch = "debug-print"} +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } snafu = { version = "0.6.3" } +# Needed for airdrop +rs_merkle = { git = "https://github.com/FloppyDisck/rs-merkle", branch = "node_export" } +# Needed for transactions +flexible-permits = {git = "https://github.com/securesecrets/flexible-permits", tag = "v1.0.1"} +remain = "0.2.2" +[dev-dependencies] +secretcli = { version = "0.1.0", path = "../secretcli" } diff --git a/packages/shade_protocol/src/airdrop/account.rs b/packages/shade_protocol/src/airdrop/account.rs new file mode 100644 index 000000000..1f2c83f71 --- /dev/null +++ b/packages/shade_protocol/src/airdrop/account.rs @@ -0,0 +1,53 @@ +use cosmwasm_std::{HumanAddr, StdError, StdResult, Uint128}; +use flexible_permits::permit::{bech32_to_canonical, Permit}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Account { + pub addresses: Vec, + pub total_claimable: Uint128, +} + +// Used for querying account information +pub type AccountPermit = Permit; + +#[remain::sorted] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AccountPermitMsg { + pub contract: HumanAddr, + pub key: String, +} + +// Used to prove ownership over IBC addresses +pub type AddressProofPermit = Permit; + +pub fn authenticate_ownership(permit: &AddressProofPermit) -> StdResult { + let permit_address = permit.params.address.clone(); + let signer_address = permit.validate()?.as_canonical(); + if signer_address != bech32_to_canonical(permit_address.as_str()) { + return Err(StdError::generic_err(format!( + "{:?} is not the message signer", + permit_address.as_str() + ))); + } + Ok(permit_address) +} + +#[remain::sorted] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AddressProofMsg { + // Address is necessary since we have other network permits present + pub address: HumanAddr, + // Reward amount + pub amount: Uint128, + // Used to prevent permits from being used elsewhere + pub contract: HumanAddr, + // Index of the address in the leaves array + pub index: u32, + // Used to identify permits + pub key: String, +} diff --git a/packages/shade_protocol/src/airdrop/claim_info.rs b/packages/shade_protocol/src/airdrop/claim_info.rs new file mode 100644 index 000000000..fcce4ae33 --- /dev/null +++ b/packages/shade_protocol/src/airdrop/claim_info.rs @@ -0,0 +1,10 @@ +use cosmwasm_std::{HumanAddr, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct RequiredTask { + pub address: HumanAddr, + pub percent: Uint128, +} diff --git a/packages/shade_protocol/src/airdrop/mod.rs b/packages/shade_protocol/src/airdrop/mod.rs new file mode 100644 index 000000000..77c297be7 --- /dev/null +++ b/packages/shade_protocol/src/airdrop/mod.rs @@ -0,0 +1,180 @@ +pub mod account; +pub mod claim_info; +use crate::{ + airdrop::{ + account::{AccountPermit, AddressProofPermit}, + claim_info::RequiredTask, + }, + asset::Contract, + generic_response::ResponseStatus, +}; +use cosmwasm_std::{Binary, HumanAddr, Uint128}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Config { + pub admin: HumanAddr, + // Used for permit validation when querying + pub contract: HumanAddr, + // Where the decayed tokens will be dumped, if none then nothing happens + pub dump_address: Option, + // The snip20 to be minted + pub airdrop_snip20: Contract, + // Airdrop amount + pub airdrop_amount: Uint128, + // Required tasks + pub task_claim: Vec, + // Checks if airdrop has started / ended + pub start_date: u64, + // Airdrop stops at end date if there is one + pub end_date: Option, + // Starts to decay at this date + pub decay_start: Option, + // This is necessary to validate the airdrop information + // tree root + pub merkle_root: Binary, + // tree height + pub total_accounts: u32, + // max possible reward amount; used to prevent collision possibility + pub max_amount: Uint128, + // Protects from leaking user information by limiting amount detail + pub query_rounding: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitMsg { + pub admin: Option, + // Where the decayed tokens will be dumped, if none then nothing happens + pub dump_address: Option, + pub airdrop_token: Contract, + // Airdrop amount + pub airdrop_amount: Uint128, + // The airdrop time limit + pub start_date: Option, + // Can be set to never end + pub end_date: Option, + // Starts to decay at this date + pub decay_start: Option, + // Base64 encoded version of the tree root + pub merkle_root: Binary, + // Root height + pub total_accounts: u32, + // Max possible reward amount + pub max_amount: Uint128, + // Default gifted amount + pub default_claim: Uint128, + // The task related claims + pub task_claim: Vec, + // Protects from leaking user information by limiting amount detail + pub query_rounding: Uint128, +} + +impl InitCallback for InitMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + UpdateConfig { + admin: Option, + dump_address: Option, + query_rounding: Option, + start_date: Option, + end_date: Option, + decay_start: Option, + padding: Option, + }, + AddTasks { + tasks: Vec, + padding: Option, + }, + CompleteTask { + address: HumanAddr, + padding: Option, + }, + CreateAccount { + addresses: Vec, + partial_tree: Vec, + padding: Option, + }, + /// Adds more addresses to accounts + UpdateAccount { + addresses: Vec, + partial_tree: Vec, + padding: Option, + }, + DisablePermitKey { + key: String, + padding: Option, + }, + Claim { + padding: Option, + }, + ClaimDecay { + padding: Option, + }, +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + UpdateConfig { status: ResponseStatus }, + AddTask { status: ResponseStatus }, + CompleteTask { status: ResponseStatus }, + CreateAccount { status: ResponseStatus }, + UpdateAccount { status: ResponseStatus }, + DisablePermitKey { status: ResponseStatus }, + Claim { status: ResponseStatus }, + ClaimDecay { status: ResponseStatus }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + Config {}, + Dates { + current_date: Option, + }, + TotalClaimed {}, + Account { + permit: AccountPermit, + current_date: Option, + }, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + Config { + config: Config, + }, + Dates { + start: u64, + end: Option, + decay_start: Option, + decay_factor: Option, + }, + TotalClaimed { + claimed: Uint128, + }, + Account { + // Total eligible + total: Uint128, + // Total claimed + claimed: Uint128, + // Total unclaimed but available + unclaimed: Uint128, + finished_tasks: Vec, + }, +} diff --git a/packages/shade_protocol/src/asset.rs b/packages/shade_protocol/src/asset.rs index 1af7fb3d1..6b6cd8196 100644 --- a/packages/shade_protocol/src/asset.rs +++ b/packages/shade_protocol/src/asset.rs @@ -1,6 +1,6 @@ +use cosmwasm_std::HumanAddr; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cosmwasm_std::HumanAddr; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/packages/shade_protocol/src/band.rs b/packages/shade_protocol/src/band.rs index 78d1dcb54..f34bdcc46 100644 --- a/packages/shade_protocol/src/band.rs +++ b/packages/shade_protocol/src/band.rs @@ -1,12 +1,23 @@ -use cosmwasm_std::{Uint128}; -use serde::{Deserialize, Serialize}; +use cosmwasm_std::Uint128; use schemars::JsonSchema; -use secret_toolkit::utils::Query; +use secret_toolkit::utils::{InitCallback, Query}; +#[cfg(test)] +use secretcli::secretcli::{TestHandle, TestInit, TestQuery}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitMsg {} + +impl InitCallback for InitMsg { + const BLOCK_SIZE: usize = 256; +} + +#[cfg(test)] +impl TestInit for InitMsg {} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum BandQuery { - GetReferenceData { base_symbol: String, quote_symbol: String, diff --git a/packages/shade_protocol/src/generic_response.rs b/packages/shade_protocol/src/generic_response.rs index 0c7eb5bb7..35fd54318 100644 --- a/packages/shade_protocol/src/generic_response.rs +++ b/packages/shade_protocol/src/generic_response.rs @@ -1,9 +1,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ResponseStatus { Success, Failure, -} \ No newline at end of file +} diff --git a/packages/shade_protocol/src/governance/mod.rs b/packages/shade_protocol/src/governance/mod.rs new file mode 100644 index 000000000..2c89fc4d7 --- /dev/null +++ b/packages/shade_protocol/src/governance/mod.rs @@ -0,0 +1,242 @@ +pub mod proposal; +pub mod vote; + +use crate::{asset::Contract, generic_response::ResponseStatus}; +use cosmwasm_std::{Binary, HumanAddr, Uint128}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +use serde::{Deserialize, Serialize}; + +// This is used when calling itself +pub const GOVERNANCE_SELF: &str = "SELF"; + +// Admin command variable spot +pub const ADMIN_COMMAND_VARIABLE: &str = "{}"; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Config { + pub admin: HumanAddr, + // Staking contract - optional to support admin only + pub staker: Option, + // The token allowed for funding + pub funding_token: Contract, + // The amount required to fund a proposal + pub funding_amount: Uint128, + // Proposal funding period deadline + pub funding_deadline: u64, + // Proposal voting period deadline + pub voting_deadline: u64, + // The minimum total amount of votes needed to approve deadline + pub minimum_votes: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AdminCommand { + pub msg: String, + pub total_arguments: u16, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct InitMsg { + pub admin: Option, + pub staker: Option, + pub funding_token: Contract, + pub funding_amount: Uint128, + pub funding_deadline: u64, + pub voting_deadline: u64, + pub quorum: Uint128, +} + +impl InitCallback for InitMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + /// Generic proposal + CreateProposal { + // Contract that will be run + target_contract: String, + // This will be saved as binary + proposal: String, + description: String, + }, + + /// Proposal funding + Receive { + sender: HumanAddr, + amount: Uint128, + // Proposal ID + msg: Option, + }, + + /// Admin Command + /// These commands can be run by admins any time + AddAdminCommand { + name: String, + proposal: String, + }, + RemoveAdminCommand { + name: String, + }, + UpdateAdminCommand { + name: String, + proposal: String, + }, + TriggerAdminCommand { + target: String, + command: String, + variables: Vec, + description: String, + }, + + /// Config changes + UpdateConfig { + admin: Option, + staker: Option, + proposal_deadline: Option, + funding_amount: Option, + funding_deadline: Option, + minimum_votes: Option, + }, + + DisableStaker {}, + + // RequestMigration {} + /// Add a contract to send proposal msgs to + AddSupportedContract { + name: String, + contract: Contract, + }, + RemoveSupportedContract { + name: String, + }, + UpdateSupportedContract { + name: String, + contract: Contract, + }, + + /// Proposal voting - can only be done by staking contract + MakeVote { + voter: HumanAddr, + proposal_id: Uint128, + votes: vote::VoteTally, + }, + + /// Trigger proposal + TriggerProposal { + proposal_id: Uint128, + }, +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + CreateProposal { + status: ResponseStatus, + proposal_id: Uint128, + }, + FundProposal { + status: ResponseStatus, + total_funding: Uint128, + }, + AddAdminCommand { + status: ResponseStatus, + }, + RemoveAdminCommand { + status: ResponseStatus, + }, + UpdateAdminCommand { + status: ResponseStatus, + }, + TriggerAdminCommand { + status: ResponseStatus, + proposal_id: Uint128, + }, + UpdateConfig { + status: ResponseStatus, + }, + DisableStaker { + status: ResponseStatus, + }, + AddSupportedContract { + status: ResponseStatus, + }, + RemoveSupportedContract { + status: ResponseStatus, + }, + UpdateSupportedContract { + status: ResponseStatus, + }, + MakeVote { + status: ResponseStatus, + }, + TriggerProposal { + status: ResponseStatus, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetProposalVotes { + proposal_id: Uint128, + }, + GetProposals { + start: Uint128, + end: Uint128, + status: Option, + }, + GetProposal { + proposal_id: Uint128, + }, + GetTotalProposals {}, + GetSupportedContracts {}, + GetSupportedContract { + name: String, + }, + GetAdminCommands {}, + GetAdminCommand { + name: String, + }, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + ProposalVotes { + status: vote::VoteTally, + }, + Proposals { + proposals: Vec, + }, + Proposal { + proposal: proposal::QueriedProposal, + }, + TotalProposals { + total: Uint128, + }, + SupportedContracts { + contracts: Vec, + }, + SupportedContract { + contract: Contract, + }, + AdminCommands { + commands: Vec, + }, + AdminCommand { + command: AdminCommand, + }, +} diff --git a/packages/shade_protocol/src/governance/proposal.rs b/packages/shade_protocol/src/governance/proposal.rs new file mode 100644 index 000000000..02d1b3164 --- /dev/null +++ b/packages/shade_protocol/src/governance/proposal.rs @@ -0,0 +1,48 @@ +use crate::generic_response::ResponseStatus; +use cosmwasm_std::{Binary, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Proposal { + // Proposal ID + pub id: Uint128, + // Target smart contract + pub target: String, + // Message to execute + pub msg: Binary, + // Description of proposal + pub description: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct QueriedProposal { + pub id: Uint128, + pub target: String, + pub msg: Binary, + pub description: String, + pub funding_deadline: u64, + pub voting_deadline: Option, + pub total_funding: Uint128, + pub status: ProposalStatus, + pub run_status: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProposalStatus { + // Admin command called + AdminRequested, + // In funding period + Funding, + // Voting in progress + Voting, + // Total votes did not reach minimum total votes + Expired, + // Majority voted No + Rejected, + // Majority votes yes + Passed, +} diff --git a/packages/shade_protocol/src/governance/vote.rs b/packages/shade_protocol/src/governance/vote.rs new file mode 100644 index 000000000..df78a7f58 --- /dev/null +++ b/packages/shade_protocol/src/governance/vote.rs @@ -0,0 +1,27 @@ +use cosmwasm_std::Uint128; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct VoteTally { + pub yes: Uint128, + pub no: Uint128, + pub abstain: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Vote { + Yes, + No, + Abstain, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +/// Used to give weight to votes per user +pub struct UserVote { + pub vote: Vote, + pub weight: u8, +} diff --git a/packages/shade_protocol/src/initializer.rs b/packages/shade_protocol/src/initializer.rs index 4f497ce88..161b6052e 100644 --- a/packages/shade_protocol/src/initializer.rs +++ b/packages/shade_protocol/src/initializer.rs @@ -1,7 +1,9 @@ +use crate::snip20::InitialBalance; +use cosmwasm_std::{Binary, HumanAddr}; use schemars::JsonSchema; +#[cfg(test)] +use secretcli::secretcli::{TestHandle, TestInit, TestQuery}; use serde::{Deserialize, Serialize}; -use crate::snip20::InitialBalance; -use cosmwasm_std::{HumanAddr, Binary}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InitializerConfig { @@ -30,19 +32,24 @@ pub struct InitMsg { pub silk: Snip20ContractInfo, } +#[cfg(test)] +impl TestInit for InitMsg {} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum HandleMsg { -} +pub enum HandleMsg {} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - Contracts {}, + GetContracts {}, } +#[cfg(test)] +impl TestQuery for QueryMsg {} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct ContractsAnswer { - pub contracts: Vec, -} \ No newline at end of file +pub enum QueryAnswer { + Contracts { contracts: Vec }, +} diff --git a/packages/shade_protocol/src/lib.rs b/packages/shade_protocol/src/lib.rs index f26133dc7..a9e78b0c0 100644 --- a/packages/shade_protocol/src/lib.rs +++ b/packages/shade_protocol/src/lib.rs @@ -1,10 +1,20 @@ +// Helper libraries pub mod asset; -pub mod mint; -pub mod micro_mint; -pub mod oracle; -pub mod treasury; +pub mod band; pub mod generic_response; +pub mod math; pub mod secretswap; -pub mod band; pub mod snip20; + +// Protocol init libraries +pub mod airdrop; pub mod initializer; + +// Protocol libraries +pub mod governance; +pub mod micro_mint; +pub mod mint; +pub mod oracle; +pub mod scrt_staking; +pub mod staking; +pub mod treasury; diff --git a/packages/shade_protocol/src/math.rs b/packages/shade_protocol/src/math.rs new file mode 100644 index 000000000..4bf9fcae8 --- /dev/null +++ b/packages/shade_protocol/src/math.rs @@ -0,0 +1,40 @@ +use cosmwasm_std::{StdError, StdResult, Uint128}; + +// Non permanent solutions to Uint128 issues + +pub fn mult(a: Uint128, b: Uint128) -> Uint128 { + if a.is_zero() || b.is_zero() { + return Uint128::zero(); + } + + Uint128(a.u128() * b.u128()) +} + +pub fn div(nom: Uint128, den: Uint128) -> StdResult { + if den == Uint128::zero() { + return Err(StdError::generic_err("Division by 0")); + } + + Ok(Uint128(nom.u128() / den.u128())) +} + +#[cfg(test)] +pub mod tests { + use crate::math::{div, mult}; + use cosmwasm_std::Uint128; + + #[test] + fn multiply() { + assert_eq!(Uint128(10), mult(Uint128(5), Uint128(2))) + } + + #[test] + fn divide() { + assert_eq!(Uint128(5), div(Uint128(10), Uint128(2)).unwrap()) + } + + #[test] + fn divide_by_zero() { + assert!(div(Uint128(10), Uint128(0)).is_err()) + } +} diff --git a/packages/shade_protocol/src/micro_mint.rs b/packages/shade_protocol/src/micro_mint.rs new file mode 100644 index 000000000..3566ed5cc --- /dev/null +++ b/packages/shade_protocol/src/micro_mint.rs @@ -0,0 +1,149 @@ +use crate::{asset::Contract, generic_response::ResponseStatus, snip20::Snip20Asset}; +use cosmwasm_std::{Binary, HumanAddr, Uint128}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Config { + pub admin: HumanAddr, + pub oracle: Contract, + // Both treasury & Commission must be set to function + pub treasury: Option, + pub secondary_burn: Option, + pub activated: bool, +} + +/// Used to store the assets allowed to be burned +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct SupportedAsset { + pub asset: Snip20Asset, + // Commission percentage * 100 e.g. 5 == .05 == 5% + pub capture: Uint128, +} + +// Used to keep track of the cap +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct MintLimit { + pub frequency: u64, + pub mint_capacity: Uint128, + pub total_minted: Uint128, + pub next_epoch: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitMsg { + pub admin: Option, + pub native_asset: Contract, + pub oracle: Contract, + //Symbol to peg to, default to snip20 symbol + pub peg: Option, + // Both treasury & capture must be set to function + pub treasury: Option, + // This is where the non-burnable assets will go, if not defined they will stay in this contract + pub secondary_burn: Option, + // If left blank no limit will be enforced + pub start_epoch: Option, + pub epoch_frequency: Option, + pub epoch_mint_limit: Option, +} + +impl InitCallback for InitMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + UpdateConfig { + admin: Option, + oracle: Option, + treasury: Option, + secondary_burn: Option, + }, + UpdateMintLimit { + start_epoch: Option, + epoch_frequency: Option, + epoch_limit: Option, + }, + RegisterAsset { + contract: Contract, + // Commission * 100 e.g. 5 == .05 == 5% + capture: Option, + }, + RemoveAsset { + address: HumanAddr, + }, + Receive { + sender: HumanAddr, + from: HumanAddr, + amount: Uint128, + memo: Option, + msg: Option, + }, +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + Init { + status: ResponseStatus, + address: HumanAddr, + }, + UpdateConfig { + status: ResponseStatus, + }, + UpdateMintLimit { + status: ResponseStatus, + }, + RegisterAsset { + status: ResponseStatus, + }, + RemoveAsset { + status: ResponseStatus, + }, + Burn { + status: ResponseStatus, + mint_amount: Uint128, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetNativeAsset {}, + GetSupportedAssets {}, + GetAsset { contract: String }, + GetConfig {}, + GetMintLimit {}, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + NativeAsset { + asset: Snip20Asset, + peg: String, + }, + SupportedAssets { + assets: Vec, + }, + Asset { + asset: SupportedAsset, + burned: Uint128, + }, + Config { + config: Config, + }, + MintLimit { + limit: MintLimit, + }, +} diff --git a/packages/shade_protocol/src/micro_mint/mod.rs b/packages/shade_protocol/src/micro_mint/mod.rs deleted file mode 100644 index a8276524e..000000000 --- a/packages/shade_protocol/src/micro_mint/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use cosmwasm_std::{HumanAddr, Uint128, Binary}; -use secret_toolkit::utils::{InitCallback, HandleCallback, Query}; -use crate::{ - snip20::Snip20Asset, - asset::Contract, - generic_response::ResponseStatus, -}; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Config { - pub owner: HumanAddr, - pub oracle: Contract, - // Both treasury & Commission must be set to function - pub treasury: Option, - pub activated: bool, -} - - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct SupportedAsset { - pub asset: Snip20Asset, - // Commission percentage * 100 e.g. 5 == .05 == 5% - pub commission: Uint128, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct InitMsg { - pub admin: Option, - pub native_asset: Contract, - pub oracle: Contract, - //Symbol to peg to, default to snip20 symbol - pub peg: Option, - // Both treasury & commission must be set to function - pub treasury: Option, -} - -impl InitCallback for InitMsg { - const BLOCK_SIZE: usize = 256; -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum HandleMsg { - UpdateConfig { - owner: Option, - oracle: Option, - treasury: Option, - }, - RegisterAsset { - contract: Contract, - // Commission * 100 e.g. 5 == .05 == 5% - commission: Option, - }, - Receive { - sender: HumanAddr, - from: HumanAddr, - amount: Uint128, - memo: Option, - msg: Option, - }, -} - -impl HandleCallback for HandleMsg { - const BLOCK_SIZE: usize = 256; -} - -#[derive(Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum HandleAnswer { - Init { status: ResponseStatus, address: HumanAddr }, - UpdateConfig { status: ResponseStatus}, - RegisterAsset { status: ResponseStatus}, - Burn { status: ResponseStatus, mint_amount: Uint128 } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum QueryMsg { - GetNativeAsset {}, - GetSupportedAssets {}, - GetAsset { - contract: String, - }, - GetConfig {}, -} - -impl Query for QueryMsg { - const BLOCK_SIZE: usize = 256; -} - -#[derive(Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum QueryAnswer { - NativeAsset { asset: Snip20Asset, peg: String }, - SupportedAssets { assets: Vec, }, - Asset { asset: SupportedAsset, burned: Uint128}, - Config { config: Config }, -} - diff --git a/packages/shade_protocol/src/mint.rs b/packages/shade_protocol/src/mint.rs index b191bd299..211dac15c 100644 --- a/packages/shade_protocol/src/mint.rs +++ b/packages/shade_protocol/src/mint.rs @@ -1,9 +1,10 @@ +use crate::{asset::Contract, generic_response::ResponseStatus}; +use cosmwasm_std::{Binary, HumanAddr, Uint128}; use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +#[cfg(test)] +use secretcli::secretcli::{TestHandle, TestInit, TestQuery}; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{HumanAddr, Uint128, Binary}; -use crate::asset::Contract; -use crate::generic_response::ResponseStatus; -use secret_toolkit::utils::{InitCallback, HandleCallback, Query}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct MintConfig { @@ -32,6 +33,9 @@ impl InitCallback for InitMsg { const BLOCK_SIZE: usize = 256; } +#[cfg(test)] +impl TestInit for InitMsg {} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleMsg { @@ -63,6 +67,9 @@ impl HandleCallback for HandleMsg { const BLOCK_SIZE: usize = 256; } +#[cfg(test)] +impl TestHandle for HandleMsg {} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct SnipMsgHook { @@ -79,20 +86,30 @@ pub struct MintMsgHook { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleAnswer { - Init { status: ResponseStatus, address: HumanAddr }, - Migrate { status: ResponseStatus }, - UpdateConfig { status: ResponseStatus}, - RegisterAsset { status: ResponseStatus}, - Burn { status: ResponseStatus, mint_amount: Uint128 } + Init { + status: ResponseStatus, + address: HumanAddr, + }, + Migrate { + status: ResponseStatus, + }, + UpdateConfig { + status: ResponseStatus, + }, + RegisterAsset { + status: ResponseStatus, + }, + Burn { + status: ResponseStatus, + mint_amount: Uint128, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { GetSupportedAssets {}, - GetAsset { - contract: String, - }, + GetAsset { contract: String }, GetConfig {}, } @@ -100,10 +117,13 @@ impl Query for QueryMsg { const BLOCK_SIZE: usize = 256; } +#[cfg(test)] +impl TestQuery for QueryMsg {} + #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryAnswer { - SupportedAssets { assets: Vec, }, + SupportedAssets { assets: Vec }, Asset { asset: SupportedAsset }, Config { config: MintConfig }, } diff --git a/packages/shade_protocol/src/oracle.rs b/packages/shade_protocol/src/oracle.rs index a2cc38264..195d7e8da 100644 --- a/packages/shade_protocol/src/oracle.rs +++ b/packages/shade_protocol/src/oracle.rs @@ -1,12 +1,11 @@ -use cosmwasm_std::{HumanAddr}; +use crate::{asset::Contract, generic_response::ResponseStatus, snip20::Snip20Asset}; +use cosmwasm_std::{HumanAddr, Uint128}; use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; use serde::{Deserialize, Serialize}; -use secret_toolkit::utils::{InitCallback, HandleCallback, Query}; -use crate::{ - asset::Contract, - generic_response::ResponseStatus, - snip20::Snip20Asset, -}; + +#[cfg(test)] +use secretcli::secretcli::{TestHandle, TestInit, TestQuery}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct SswapPair { @@ -16,9 +15,16 @@ pub struct SswapPair { pub asset: Snip20Asset, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct IndexElement { + pub symbol: String, + //TODO: Decimal, when better implementation is available + pub weight: Uint128, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct OracleConfig { - pub owner: HumanAddr, + pub admin: HumanAddr, pub band: Contract, pub sscrt: Contract, } @@ -34,45 +40,63 @@ impl InitCallback for InitMsg { const BLOCK_SIZE: usize = 256; } +#[cfg(test)] +impl TestInit for InitMsg {} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleMsg { UpdateConfig { - owner: Option, + admin: Option, band: Option, }, - // Register Secret Swap Pair (should be */SCRT) + // Register Secret Swap Pair (should be */sSCRT or sSCRT/*) RegisterSswapPair { pair: Contract, }, + // Unregister Secret Swap Pair (opposite action to RegisterSswapPair) + UnregisterSswapPair { + pair: Contract, + }, + RegisterIndex { + symbol: String, + basket: Vec, + }, } impl HandleCallback for HandleMsg { const BLOCK_SIZE: usize = 256; } +#[cfg(test)] +impl TestHandle for HandleMsg {} + #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleAnswer { - UpdateConfig { status: ResponseStatus}, - RegisterSswapPair { status: ResponseStatus}, + UpdateConfig { status: ResponseStatus }, + RegisterSswapPair { status: ResponseStatus }, + UnregisterSswapPair { status: ResponseStatus }, + RegisterIndex { status: ResponseStatus }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - GetPrice { symbol: String }, - GetConfig {}, + Config {}, + Price { symbol: String }, + Prices { symbols: Vec }, } impl Query for QueryMsg { const BLOCK_SIZE: usize = 256; } +#[cfg(test)] +impl TestQuery for QueryMsg {} + #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryAnswer { Config { config: OracleConfig }, } - - diff --git a/packages/shade_protocol/src/scrt_staking.rs b/packages/shade_protocol/src/scrt_staking.rs new file mode 100644 index 000000000..28c664b52 --- /dev/null +++ b/packages/shade_protocol/src/scrt_staking.rs @@ -0,0 +1,110 @@ +use crate::{asset::Contract, generic_response::ResponseStatus}; +use cosmwasm_std::{Binary, Decimal, FullDelegation, HumanAddr, Uint128, Validator}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Config { + pub admin: HumanAddr, + pub treasury: HumanAddr, + pub sscrt: Contract, + pub validator_bounds: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct ValidatorBounds { + pub min_commission: Decimal, + pub max_commission: Decimal, + pub top_position: Uint128, + pub bottom_position: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitMsg { + pub admin: Option, + pub treasury: HumanAddr, + pub sscrt: Contract, + pub validator_bounds: Option, + pub viewing_key: String, +} + +impl InitCallback for InitMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + UpdateConfig { + admin: Option, + }, + Receive { + sender: HumanAddr, + from: HumanAddr, + amount: Uint128, + memo: Option, + msg: Option, + }, + // Begin unbonding amount + Unbond { + validator: HumanAddr, + }, + //TODO: switch to this interface for standardization + //Claim { amount: Uint128 }, + + // Claim all pending rewards & completed unbondings + Claim { + validator: HumanAddr, + }, +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + Init { + status: ResponseStatus, + address: HumanAddr, + }, + UpdateConfig { + status: ResponseStatus, + }, + Receive { + status: ResponseStatus, + validator: Validator, + }, + Claim { + status: ResponseStatus, + }, + Unbond { + status: ResponseStatus, + delegation: FullDelegation, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetConfig {}, + //TODO: find a way to query this and return + //Unbondings {}, + Delegations {}, + Delegation { validator: HumanAddr }, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + Config { config: Config }, + Balance { amount: Uint128 }, +} diff --git a/packages/shade_protocol/src/secretswap.rs b/packages/shade_protocol/src/secretswap.rs index 3e40d80ca..23e2053ae 100644 --- a/packages/shade_protocol/src/secretswap.rs +++ b/packages/shade_protocol/src/secretswap.rs @@ -1,10 +1,8 @@ +use crate::asset::Contract; use cosmwasm_std::{HumanAddr, Uint128}; -use serde::{Deserialize, Serialize}; use schemars::JsonSchema; use secret_toolkit::utils::Query; -use crate::{ - asset::Contract, -}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -35,12 +33,10 @@ pub struct Simulation { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum PairQuery{ +pub enum PairQuery { Pair {}, Pool {}, - Simulation { - offer_asset: Asset, - }, + Simulation { offer_asset: Asset }, //ReverseSimulation {}, } diff --git a/packages/shade_protocol/src/snip20.rs b/packages/shade_protocol/src/snip20.rs index 70c557655..efa469551 100644 --- a/packages/shade_protocol/src/snip20.rs +++ b/packages/shade_protocol/src/snip20.rs @@ -1,11 +1,13 @@ +use crate::asset::Contract; +use cosmwasm_std::{Binary, HumanAddr, Querier, StdResult, Uint128}; use schemars::JsonSchema; +use secret_toolkit::{ + snip20::TokenInfo, + utils::{HandleCallback, InitCallback, Query}, +}; +#[cfg(test)] +use secretcli::secretcli::{TestHandle, TestInit, TestQuery}; use serde::{Deserialize, Serialize}; -use crate::asset::Contract; -use secret_toolkit::{snip20::TokenInfo, - utils::Query}; - -use cosmwasm_std::{StdResult, StdError, Querier, HumanAddr, Uint128, Binary}; -use secret_toolkit::utils::InitCallback; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -41,13 +43,9 @@ pub struct TokenConfigResponse { pub token_config: TokenConfig, } -pub fn token_config_query( - querier: &Q, - contract: Contract, -) -> StdResult { - let answer: TokenConfigResponse = Snip20Query::TokenConfig{}.query(querier, - contract.code_hash, - contract.address)?; +pub fn token_config_query(querier: &Q, contract: Contract) -> StdResult { + let answer: TokenConfigResponse = + Snip20Query::TokenConfig {}.query(querier, contract.code_hash, contract.address)?; Ok(answer.token_config) } @@ -73,6 +71,9 @@ impl InitCallback for InitMsg { const BLOCK_SIZE: usize = 256; } +#[cfg(test)] +impl TestInit for InitMsg {} + #[derive(Serialize, Deserialize, JsonSchema, Clone, Default, PartialEq, Debug)] #[serde(rename_all = "snake_case")] pub struct InitConfig { @@ -82,3 +83,141 @@ pub struct InitConfig { pub enable_mint: Option, pub enable_burn: Option, } + +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + ChangeAdmin { + address: HumanAddr, + padding: Option, + }, + // Native coin interactions + Redeem { + amount: Uint128, + denom: Option, + padding: Option, + }, + Deposit { + padding: Option, + }, + + // Base ERC-20 stuff + Transfer { + recipient: HumanAddr, + amount: Uint128, + memo: Option, + padding: Option, + }, + Send { + recipient: HumanAddr, + amount: Uint128, + msg: Option, + memo: Option, + padding: Option, + }, + Burn { + amount: Uint128, + memo: Option, + padding: Option, + }, + RegisterReceive { + code_hash: String, + padding: Option, + }, + CreateViewingKey { + entropy: String, + padding: Option, + }, + SetViewingKey { + key: String, + padding: Option, + }, + // Mint + Mint { + recipient: HumanAddr, + amount: Uint128, + memo: Option, + padding: Option, + }, + AddMinters { + minters: Vec, + padding: Option, + }, + RemoveMinters { + minters: Vec, + padding: Option, + }, + SetMinters { + minters: Vec, + padding: Option, + }, +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[cfg(test)] +impl TestHandle for HandleMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + TokenInfo {}, + TokenConfig {}, + ExchangeRate {}, + Allowance { + owner: HumanAddr, + spender: HumanAddr, + key: String, + }, + Balance { + address: HumanAddr, + key: String, + }, + Minters {}, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + TokenInfo { + name: String, + symbol: String, + decimals: u8, + total_supply: Option, + }, + TokenConfig { + public_total_supply: bool, + deposit_enabled: bool, + redeem_enabled: bool, + mint_enabled: bool, + burn_enabled: bool, + }, + ExchangeRate { + rate: Uint128, + denom: String, + }, + Allowance { + spender: HumanAddr, + owner: HumanAddr, + allowance: Uint128, + expiration: Option, + }, + Balance { + amount: Uint128, + }, + ViewingKeyError { + msg: String, + }, + Minters { + minters: Vec, + }, +} + +#[cfg(test)] +impl TestQuery for QueryMsg {} diff --git a/packages/shade_protocol/src/staking/mod.rs b/packages/shade_protocol/src/staking/mod.rs new file mode 100644 index 000000000..00d971598 --- /dev/null +++ b/packages/shade_protocol/src/staking/mod.rs @@ -0,0 +1,108 @@ +pub mod stake; +use crate::{asset::Contract, generic_response::ResponseStatus, governance::vote::UserVote}; +use cosmwasm_std::{HumanAddr, Uint128}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, Query}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Config { + pub admin: Contract, + // Time to unbond + pub unbond_time: u64, + // Supported staking token + pub staked_token: Contract, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct InitMsg { + pub admin: Option, + pub unbond_time: u64, + pub staked_token: Contract, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + UpdateConfig { + admin: Option, + unbond_time: Option, + }, + // Stake + Receive { + sender: HumanAddr, + from: HumanAddr, + amount: Uint128, + }, + Unbond { + amount: Uint128, + }, + // While secure querying is resolved + Vote { + proposal_id: Uint128, + votes: Vec, + }, + ClaimUnbond {}, + ClaimRewards {}, + SetViewingKey { + key: String, + }, +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + UpdateUnbondTime { status: ResponseStatus }, + Stake { status: ResponseStatus }, + Unbond { status: ResponseStatus }, + Vote { status: ResponseStatus }, + ClaimUnbond { status: ResponseStatus }, + ClaimRewards { status: ResponseStatus }, + SetViewingKey { status: ResponseStatus }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + Config {}, + TotalStaked {}, + TotalUnbonding { + start: Option, + end: Option, + }, + UserStake { + address: HumanAddr, + key: String, + time: u64, + }, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + Config { + config: Config, + }, + TotalStaked { + total: Uint128, + }, + TotalUnbonding { + total: Uint128, + }, + UserStake { + staked: Uint128, + pending_rewards: Uint128, + unbonding: Uint128, + unbonded: Uint128, + }, +} diff --git a/packages/shade_protocol/src/staking/stake.rs b/packages/shade_protocol/src/staking/stake.rs new file mode 100644 index 000000000..3f7acbf2b --- /dev/null +++ b/packages/shade_protocol/src/staking/stake.rs @@ -0,0 +1,38 @@ +use cosmwasm_std::Uint128; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Stake { + pub total_shares: Uint128, + pub total_tokens: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct UserStake { + pub shares: Uint128, + // This is used to derive the actual value to recover + pub tokens_staked: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Unbonding { + pub amount: Uint128, + pub unbond_time: u64, +} + +impl Ord for Unbonding { + fn cmp(&self, other: &Unbonding) -> Ordering { + self.unbond_time.cmp(&other.unbond_time) + } +} + +impl PartialOrd for Unbonding { + fn partial_cmp(&self, other: &Unbonding) -> Option { + Some(self.cmp(other)) + } +} diff --git a/packages/shade_protocol/src/treasury.rs b/packages/shade_protocol/src/treasury.rs index 8b0d611b7..89066fe53 100644 --- a/packages/shade_protocol/src/treasury.rs +++ b/packages/shade_protocol/src/treasury.rs @@ -1,20 +1,30 @@ +use crate::{asset::Contract, generic_response::ResponseStatus}; +use cosmwasm_std::{Binary, Decimal, HumanAddr, Uint128}; use schemars::JsonSchema; +use secret_toolkit::{ + snip20, + utils::{HandleCallback, InitCallback, Query}, +}; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{HumanAddr, Uint128, Binary}; -use crate::asset::Contract; -use crate::generic_response::ResponseStatus; -use secret_toolkit::{snip20, utils::{InitCallback, HandleCallback, Query}}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct TreasuryConfig { +pub struct Config { pub owner: HumanAddr, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct Snip20Asset { +pub struct Asset { pub contract: Contract, pub token_info: snip20::TokenInfo, + pub allocations: Option>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Allocation { + pub contract: Contract, + pub portion: Decimal, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -42,7 +52,14 @@ pub enum HandleMsg { }, RegisterAsset { contract: Contract, + /* List of contracts/users given an allowance based on a percentage of the asset balance + * e.g. governance, LP, SKY + */ + allocations: Option>, }, + + // Trigger to re-calc asset allocations + Rebalance {}, } impl HandleCallback for HandleMsg { @@ -52,19 +69,30 @@ impl HandleCallback for HandleMsg { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleAnswer { - Init { status: ResponseStatus, address: HumanAddr }, - UpdateConfig { status: ResponseStatus }, - RegisterAsset { status: ResponseStatus }, - Receive { status: ResponseStatus }, + Init { + status: ResponseStatus, + address: HumanAddr, + }, + UpdateConfig { + status: ResponseStatus, + }, + RegisterAsset { + status: ResponseStatus, + }, + Receive { + status: ResponseStatus, + }, + Rebalance { + status: ResponseStatus, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { GetConfig {}, - GetBalance { - contract: HumanAddr, - }, + GetBalance { contract: HumanAddr }, + CanRebalance {}, } impl Query for QueryMsg { @@ -74,6 +102,7 @@ impl Query for QueryMsg { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryAnswer { - Config { config: TreasuryConfig }, + Config { config: Config }, Balance { amount: Uint128 }, + CanRebalance { possible: bool }, } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..de1af2a44 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,15 @@ +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md + +# Stable configurations +edition = "2018" +max_width = 100 +merge_derives = true +use_field_init_shorthand = true +use_try_shorthand = true + +# Nightly configurations +imports_layout = "HorizontalVertical" +imports_granularity = "Crate" +overflow_delimited_expr = true +reorder_impl_items = true +version = "Two" diff --git a/shade_protocol.py b/shade_protocol.py new file mode 100755 index 000000000..945bd0f9b --- /dev/null +++ b/shade_protocol.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +import random +#import click +import json +from sys import exit, argv +from collections import defaultdict + +from contractlib.contractlib import PreInstantiatedContract +from contractlib.contractlib import Contract +from contractlib.snip20lib import SNIP20 +from contractlib.micro_mintlib import MicroMint +from contractlib.oraclelib import Oracle +from contractlib.treasurylib import Treasury +from contractlib.utils import gen_label + +from contractlib.secretlib.secretlib import GAS_METRICS, run_command, execute_contract, query_contract + +SHADE_FILE_NAME = 'shade_protocol.json' + +# Burns for core_assets assets +entry_assets = [ + ('secretSCRT', 'SSCRT'), +] + +core_assets = [ + # Core + ('Shade', 'SHD'), + ('Silk', 'SILK'), +] + +# These will be generated into pairs of Synthetic/Stabilizer +synthetic_assets = [ + # Metals + ('Gold', 'XAU'), + ('Silver', 'XAG'), + + # Fiat + #('Euro', 'EUR'), + #('Japanese yen', 'JPY'), + #('Yuan', 'CNY'), + + # Crypto + #('Ethereum', 'ETH'), + #('Bitcoin', 'BTC'), + #('secret', 'SCRT'), + #('Dogecoin', 'DOGE'), + #('Uniswap', 'UNI'), + #('Stellar', 'XLM'), + #('PancakeSwap', 'CAKE'), + #('Band Protocol', 'BAND'), + #('Terra', 'LUNA'), + #('Cosmos', 'ATOM'), + + # TODO: Oracle: add these sources + # Stocks + # ('Tesla', 'TSLA'), + # ('Apple', 'AAPL'), + # ('Google', 'GOOG'), +] + +BURN_MAP = { + # minted: burners + 'SHD': ['SILK', 'SSCRT'], + 'SILK': ['SHD', 'SSCRT'] +} + +CAPTURE = { + 'SSCRT': 1, # 100% + 'SILK': .01, # 1% + 'SHD': .01, # 1% +} + +# normalize capture values +CAPTURE = { + k: int(v * 10000) for k, v in CAPTURE.items() +} + +# Synthetics and ARB's burn for eachother +# SILK burns for Synthetic +for _, s in synthetic_assets: + BURN_MAP[f'S{s}'] = [f'A{s}', 'SILK'] + BURN_MAP[f'A{s}'] = [f'S{s}', 'SILK'] + +# synthetic and stabilizer of each asset +synthetic_assets = [ + ('Synthetic ' + name, 'S' + symbol) + for name, symbol in synthetic_assets +] + [ + ('Stabilizer ' + name, 'A' + symbol) + for name, symbol in synthetic_assets +] + +TESTNET_BAND = { + "address": "secret1p0jtg47hhwuwgp4cjpc46m7qq6vyjhdsvy2nph", + "code_hash": "77c854ea110315d5103a42b88d3e7b296ca245d8b095e668c69997b265a75ac5" +} + +TESTNET_SSCRT = { + 'address': 'secret1s7c6xp9wltthk5r6mmavql4xld5me3g37guhsx', + 'code_hash': 'cd400fb73f5c99edbc6aab22c2593332b8c9f2ea806bf9b42e3a523f3ad06f62' +} + +# viewing_key = '4b734d7a2e71fb277a9e3355f5c56d347f1012e1a9533eb7fdbb3ceceedad5fc' +viewing_key = 'password' + +chain_config = run_command(['secretcli', 'config']) + +chain_config = { + key.strip('" '): val.strip('" ') + for key, val in + ( + line.split('=') + for line in chain_config.split('\n') + if line + ) +} +account_key = 'drpresident' if chain_config['chain-id'] == 'holodeck-2' else 'a' +backend = None if chain_config['chain-id'] == 'holodeck-2' else 'test' +account = run_command(['secretcli', 'keys', 'show', '-a', account_key]).rstrip() + +def execute(contract: dict, msg: dict): + return execute_contract(contract['address'], json.dumps(msg), user=account_key, backend=backend) + +def query(contract: dict, msg: dict): + return query_contract(contract['address'], json.dumps(msg)) + +def deploy(): + + sscrt = None + band = None + + shade_protocol = defaultdict(dict) + + shade_protocol['assets'] = defaultdict(dict) + shade_protocol['burn_map'] = BURN_MAP + + if chain_config['chain-id'] == 'holodeck-2': + print('Setting testnet SSCRT') + sscrt = PreInstantiatedContract(TESTNET_SSCRT['address'], TESTNET_SSCRT['code_hash']) + sscrt.symbol = 'SSCRT' + print(sscrt.address) + + print('Setting testnet band') + band = PreInstantiatedContract(TESTNET_BAND['address'], TESTNET_BAND['code_hash']) + print(TESTNET_BAND['address']) + + elif chain_config['chain-id'] == 'enigma-pub-testnet-3': + print('Configuring SSCRT') + sscrt = SNIP20(gen_label(8), name='secretSCRT', symbol='SSCRT', decimals=6, public_total_supply=True, enable_deposit=True, enable_burn=True, + admin=account_key, uploader=account_key, backend=backend) + print(sscrt.address) + sscrt.set_view_key(account_key, viewing_key) + + print('Mocking Band for local chain') + band = Contract('mock_band.wasm.gz', '{}', gen_label(8)) + print(band.address) + + + print('Configuring Oracle') + oracle = Oracle(gen_label(8), band, sscrt, + admin=account_key, uploader=account_key, backend=backend) + shade_protocol['oracle'] = { + 'address': oracle.address, + 'code_hash': oracle.code_hash, + } + print(oracle.address) + scrt_price = int(oracle.get_price('SCRT')['rate']) + print('SCRT', scrt_price / (10 ** 18)) + sscrt_price = int(oracle.get_price('SSCRT')['rate']) + print('SSCRT', scrt_price / (10 ** 18)) + for _, symbol in core_assets + synthetic_assets: + print(symbol, int(oracle.get_price(symbol)['rate']) / (10 ** 18)) + + print('Configuring Treasury') + treasury = Treasury(gen_label(8), + admin=account_key, uploader=account_key, backend=backend) + print(treasury.address) + shade_protocol['treasury'] = { + 'address': treasury.address, + 'code_hash': treasury.code_hash, + } + + print('Registering SSCRT with treasury') + treasury.register_asset(sscrt) + + snip20_id = None + mint_id = None + + tokens = { 'SSCRT': sscrt } + + print('\nCore Snips') + core_snips = [] + + for name, symbol in core_assets: + + print('\nConfiguring', name, symbol) + snip = SNIP20(gen_label(8), name=name, symbol=symbol, decimals=6, + public_total_supply=True, enable_mint=True, enable_burn=True, + admin=account_key, uploader=account_key, backend=backend, + code_id=snip20_id) + + if not snip20_id: + snip20_id = snip.code_id + + shade_protocol['assets'][snip.symbol]['snip20'] = { + 'address': snip.address, + 'code_hash': snip.code_hash, + } + + print(f'Registering {snip.symbol} with treasury') + treasury.register_asset(snip) + + print('Set Viewing Key') + snip.set_view_key(account_key, viewing_key) + print(snip.symbol, snip.address) + core_snips.append(snip) + tokens[symbol] = snip + + print('Core Mints') + # (snip, mint) + core_pairs = [] + for snip in core_snips: + print('Configuring', snip.symbol, 'Mint') + mint = MicroMint(gen_label(8), snip, oracle, treasury, + # initial_assets=initial_assets, + admin=account_key, uploader=account_key, backend=backend, + code_id=mint_id) + + if not mint_id: + mint_id = mint.code_id + + shade_protocol['assets'][snip.symbol]['mint'] = { + 'address': mint.address, + 'code_hash': mint.code_hash, + } + print(mint.address) + + print(f'Linking Snip/Mint {snip.symbol}') + snip.set_minters([mint.address]) + + for burn_symbol in BURN_MAP[snip.symbol]: + capture = CAPTURE.get(burn_symbol) + print(f'Registering {burn_symbol} with {snip.symbol}' + f' {capture} captured' if capture else '') + mint.register_asset(tokens[burn_symbol], capture) + + core_pairs.append((snip, mint)) + + print('\nSynthetics') + + synthetic_snips = [] + # (snip, mint) + synthetics = [] + + for name, symbol in synthetic_assets: + print('\nConfiguring', name, symbol) + snip = SNIP20(gen_label(8), name=name, symbol=symbol, + decimals=6, public_total_supply=True, + enable_mint=True, enable_burn=True, + admin=account_key, uploader=account_key, backend=backend, + code_id=snip20_id) + + shade_protocol['assets'][snip.symbol]['snip20'] = { + 'address': snip.address, + 'code_hash': snip.code_hash, + } + + print(f'Registering {snip.symbol} with treasury') + treasury.register_asset(snip) + print('Set Viewing Key') + snip.set_view_key(account_key, viewing_key) + print(snip.symbol, snip.address) + + synthetic_snips.append(snip) + tokens[symbol] = snip + + print('Synthetic Mints') + + for snip in synthetic_snips: + + print('Configuring', snip.symbol, 'Mint') + mint = MicroMint(gen_label(8), snip, oracle, treasury, + # initial_assets=initial_assets, + admin=account_key, uploader=account_key, backend=backend, + code_id=mint_id) + + shade_protocol['assets'][snip.symbol]['mint'] = { + 'address': mint.address, + 'code_hash': mint.code_hash, + } + print(mint.address) + + for burn_symbol in BURN_MAP[snip.symbol]: + capture = CAPTURE.get(burn_symbol) + print(f'Registering {burn_symbol} with {snip.symbol}' + f' {capture} capture' if capture else '') + mint.register_asset(tokens[burn_symbol], CAPTURE.get(burn_symbol)) + + print(f'Linking Snip/Mint {snip.symbol}') + snip.set_minters([mint.address]) + + synthetics.append((snip, mint)) + + print(run_command(['secretcli', 'q', 'account', account])) + + if 'prime' in argv: + + sscrt_mint_amount = '100000000000000' + print(f'\tDeposit {sscrt_mint_amount} uSCRT') + sscrt.deposit(account, sscrt_mint_amount + 'uscrt') + sscrt_minted = sscrt.get_balance(account, viewing_key) + print(f'\tReceived {sscrt_minted} usSCRT') + assert sscrt_mint_amount == sscrt_minted, f'Minted {sscrt_minted}; expected {sscrt_mint_amount}' + total_amount = 100000000000000 + minimum_amount = 1000 + send_amount = random.randint(1000, int(total_amount / len(core_pairs + synthetics)) - 1) + + # Initializing balances with sscrt + print('\nEntry minting with SSCRT') + for core, mint in core_pairs: + print(f'Burning {send_amount} usSCRT for', core.symbol) + + mint_response = sscrt.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + print('Wallet Balance') + for snip in [sscrt] + core_snips + synthetic_snips: + print('\t', snip.get_balance(account, viewing_key), snip.symbol) + + print('Treasury Balance') + print('\t', treasury.get_balance(sscrt), sscrt.symbol) + for snip in core_snips + synthetic_snips: + print('\t', treasury.get_balance(snip), snip.symbol) + + print('\nBurning core assets for eachother') + + for core, _ in core_pairs: + send_amount = int( + int(core.get_balance(account, viewing_key)) + / 10) + for c, mint in core_pairs: + if c is core: + continue + print(f'\nBurning {send_amount}', core.symbol, 'for', c.symbol) + + mint_response = core.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + # print(mint_response) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + + print('\nBurning core assets for Synthetics') + #send_amount = int(send_amount / 3) + for core, _ in core_pairs: + send_amount = int( + int(core.get_balance(account, viewing_key)) + / (len(synthetics) + 1)) + + for synthetic, mint in synthetics: + print(f'\nBurning {send_amount}', core.symbol, 'for', synthetic.symbol) + + mint_response = core.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + # print(mint_response) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + print('\nBurning Synthetics amongst eachother') + + for snip in synthetic_snips: + send_amount = int( + int(snip.get_balance(account, viewing_key)) + / (len(synthetic_snips) + 1)) + + for s, mint in synthetics: + if s is snip: + continue + print(f'\nBurning {send_amount}', synthetic.symbol, 'for', s.symbol) + + mint_response = synthetic.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + print(mint_response) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + + print('Wallet Balance') + for snip in [sscrt] + core_snips + synthetic_snips: + print('\t', snip.get_balance(account, viewing_key), snip.symbol) + + print('Treasury Balance') + print('\t', treasury.get_balance(sscrt), sscrt.symbol) + for snip in core_snips + synthetic_snips: + print('\t', treasury.get_balance(snip), snip.symbol) + + print(json.dumps(shade_protocol, indent=2)) + suffix = 'local' if account_key == 'a' else 'testnet' + + print(len(GAS_METRICS), 'times gas was used') + open(f'logs/gas_metrics_{suffix}.json', 'w+').write(json.dumps(GAS_METRICS, indent=2)) + + shade_protocol['gas_wanted'] = sum(int(g['want']) for g in GAS_METRICS) + shade_protocol['gas_used'] = sum(int(g['used']) for g in GAS_METRICS) + + + return shade_protocol + +''' +Reads in the shade_protocol.json and verifies each contract +''' +def verify(shade_protocol): + + print('ORACLE') + print(shade_protocol['oracle']) + + print('Treasury') + print(shade_protocol['treasury']) + + print('ASSETS') + for asset in shade_protocol['assets']: + print(asset) + +''' +Primes networks with user funds +''' +def prime(shade_protocol): + + ''' + sscrt_mint_amount = '100000000000000' + print(f'\tDeposit {sscrt_mint_amount} uSCRT') + sscrt.deposit(account, sscrt_mint_amount + 'uscrt') + sscrt_minted = sscrt.get_balance(account, viewing_key) + print(f'\tReceived {sscrt_minted} usSCRT') + assert sscrt_mint_amount == sscrt_minted, f'Minted {sscrt_minted}; expected {sscrt_mint_amount}' + total_amount = 100000000000000 + minimum_amount = 1000 + send_amount = random.randint(1000, int(total_amount / len(core_pairs + synthetics)) - 1) + + # Initializing balances with sscrt + print('\nEntry minting with SSCRT') + for core, mint in core_pairs: + print(f'Burning {send_amount} usSCRT for', core.symbol) + + mint_response = sscrt.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + print('Wallet Balance') + for snip in core_snips + synthetic_snips: + print('\t', snip.get_balance(account, viewing_key), snip.symbol) + + print('Treasury Balance') + print('\t', treasury.get_balance(sscrt), sscrt.symbol) + for snip in core_snips + synthetic_snips: + print('\t', treasury.get_balance(snip), snip.symbol) + + print('\nBurning core assets for eachother') + + for core, _ in core_pairs: + send_amount = int( + int(core.get_balance(account, viewing_key)) + / 10) + for c, mint in core_pairs: + if c is core: + continue + print(f'\nBurning {send_amount}', core.symbol, 'for', c.symbol) + + mint_response = core.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + # print(mint_response) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + + print('\nBurning core assets for Synthetics') + #send_amount = int(send_amount / 3) + for core, _ in core_pairs: + send_amount = int( + int(core.get_balance(account, viewing_key)) + / (len(synthetics) + 1)) + + for synthetic, mint in synthetics: + print(f'\nBurning {send_amount}', core.symbol, 'for', synthetic.symbol) + + mint_response = core.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + # print(mint_response) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + print('\nBurning Synthetics amongst eachother') + + for snip in synthetic_snips: + send_amount = int( + int(snip.get_balance(account, viewing_key)) + / (len(synthetic_snips) + 1)) + + for s, mint in synthetics: + if s is snip: + continue + print(f'\nBurning {send_amount}', synthetic.symbol, 'for', s.symbol) + + mint_response = synthetic.send(account_key, mint.address, send_amount, {'minimum_expected_amount': '0'}) + print(mint_response) + if mint_response.get('output_error'): + print(f'Mint error: {mint_response["output_error"]}') + + + print('Wallet Balance') + for snip in core_snips + synthetic_snips: + print('\t', snip.get_balance(account, viewing_key), snip.symbol) + + print('Treasury Balance') + print('\t', treasury.get_balance(sscrt), sscrt.symbol) + for snip in core_snips + synthetic_snips: + print('\t', treasury.get_balance(snip), snip.symbol) + + print(json.dumps(shade_protocol, indent=2)) + suffix = 'local' if account_key == 'a' else 'testnet' + + print(len(GAS_METRICS), 'times gas was used') + open(f'logs/gas_metrics_{suffix}.json', 'w+').write(json.dumps(GAS_METRICS, indent=2)) + + shade_protocol['gas_wanted'] = sum(int(g['want']) for g in GAS_METRICS) + shade_protocol['gas_used'] = sum(int(g['used']) for g in GAS_METRICS) + + + return shade_protocol + ''' + + +''' +Query contracts for more detailed information such as +token_info, token_config & get_config +code_id +treasury balance, snip20 balances +''' +def enrich(shade_protocol): + + print('Enriching') + print('Oracle') + shade_protocol['oracle'].update(query(shade_protocol['oracle'], {'get_config': {}})) + + print('Treasury') + shade_protocol['treasury'].update(query(shade_protocol['treasury'], {'get_config': {}})) + + for symbol, pair in shade_protocol['assets'].items(): + print(symbol) + pair['snip20'].update(query(pair['snip20'], {'token_info': {}})) + pair['snip20'].update(query(pair['snip20'], {'token_config': {}})) + pair['mint'].update(query(pair['mint'], {'get_config': {}})) + + return shade_protocol + +if __name__ == '__main__': + + shade_protocol = None + + for s in argv: + + if s == 'load': + print('Loading') + shade_protocol = json.loads(open(SHADE_FILE_NAME).read()) + + elif s == 'deploy': + print('Deploying') + shade_protocol = deploy() + + elif s == 'verify': + print('Verifying') + verify(shade_protocol) + + elif s == 'enrich': + print('Enriching') + shade_protocol = enrich(shade_protocol) + + elif s == 'prime': + print('Priming') + shade_protocol = prime(shade_protocol) + + elif s == 'save': + print('Saving') + open(SHADE_FILE_NAME, 'w+').write(json.dumps(shade_protocol, indent=2)) + + if shade_protocol: + print(json.dumps(shade_protocol, indent=2)) + else: + print('nothing to do') diff --git a/test-index.py b/test-index.py new file mode 100755 index 000000000..0b368f42e --- /dev/null +++ b/test-index.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import json +from contractlib.secretlib.secretlib import query_contract +from contractlib.contractlib import Contract +from contractlib.oraclelib import Oracle +from contractlib.snip20lib import SNIP20 +from contractlib.utils import gen_label + +sscrt = SNIP20(gen_label(8), name='secretSCRT', symbol='SSCRT', + decimals=6, public_total_supply=True, + enable_deposit=True, enable_burn=True, + admin='a', uploader='a', + backend='test') + +mock_band = Contract('mock_band.wasm.gz', '{}', gen_label(8)) +print('mock band') +print(mock_band.address) + +oracle = Oracle(gen_label(8), mock_band, sscrt, admin='a', uploader='a', backend=None) + +print('oracle') +print(oracle.address) + +band_prices = { + # symbol: price + 'USD': 1, + 'SCRT': 7.5, +} + +index_basket = { + # symbol: weight + 'USD': .2, + 'SCRT': .5, +} + +# normalize band prices +band_prices = [ + {'mock_price': {'symbol': s, 'price': str(int(p * 10**18))}} + for s, p in band_prices.items() +] +# normalize index basket +index_basket = [ + {'symbol': s, 'weight': str(int(w * 10**18))} + for s, w in index_basket.items() +] +print(json.dumps(index_basket, indent=2)) + +for b in band_prices: + print('mocking', b) + print(mock_band.execute(b)) + +print(oracle.execute({'register_index': {'symbol': 'SILK', 'basket': index_basket}})) + +print('\n'.join(oracle.query({'prices': {'symbols': ['USD', 'SCRT']}}))) + +usd = int(oracle.query({'price': {'symbol': 'USD'}})['rate']) / 10**18 +scrt = int(oracle.query({'price': {'symbol': 'SCRT'}})['rate']) / 10**18 +silk = int(oracle.query({'price': {'symbol': 'SILK'}})['rate']) / 10**18 + +print('USD:', usd) +print('SCRT:', scrt) +print('SILK:', silk) diff --git a/test-oracle.py b/test-oracle.py index 8ae03696c..c65bd83d3 100755 --- a/test-oracle.py +++ b/test-oracle.py @@ -9,6 +9,9 @@ #sscrt_snip = 'secret1s7c6xp9wltthk5r6mmavql4xld5me3g37guhsx' #sscrt_code_hash = 'cd400fb73f5c99edbc6aab22c2593332b8c9f2ea806bf9b42e3a523f3ad06f62' +USER = 'a' # drpresident +KEYRING = 'test' + silk_snip = 'secret12dlkq02clar92hrtxsd8dy54xcm088mzu2vftu' silk_pair = 'secret1j3llhgudfqtuqq3qhfflqzyhu5dgrxeeztxhqx' @@ -43,7 +46,7 @@ sim = { "simulation": { "offer_asset": { - # 1 sSCRT? + # 1 sSCRT "amount": '1000000', "info": { "token": { @@ -80,7 +83,7 @@ def sswap_price(snip20, sscrt_pair): return_amount = int(results['return_amount']) return normalize(return_amount, info['decimals']), info['symbol'] -oracle = Oracle(gen_label(8), band, sscrt, admin='drpresident', uploader='drpresident', backend=None) +oracle = Oracle(gen_label(8), band, sscrt, admin=USER, uploader=USER, backend=KEYRING) print(oracle.address) print(oracle.code_hash) ''' @@ -93,8 +96,8 @@ def sswap_price(snip20, sscrt_pair): print('Registering SETH') print(oracle.register_sswap_pair(seth_pair)) -print(oracle.get_price('SETH')['rate'], 'SETH') +print(oracle.price('SETH')['rate'], 'SETH') print('Registering SOCEAN') print(oracle.register_sswap_pair(socean_pair)) -print(oracle.get_price('SOCEAN')['rate'], 'SOCEAN') +print(oracle.price('SOCEAN')['rate'], 'SOCEAN') diff --git a/test-scrt-staking.py b/test-scrt-staking.py new file mode 100755 index 000000000..da909b7ce --- /dev/null +++ b/test-scrt-staking.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +import json +from contractlib.contractlib import Contract +from contractlib.utils import gen_label +from contractlib.secretlib.secretlib import run_command, execute_contract, query_contract +from contractlib.snip20lib import SNIP20 + +chain_config = run_command(['secretcli', 'config']) + +chain_config = { + key.strip('" '): val.strip('" ') + for key, val in + ( + line.split('=') + for line in chain_config.split('\n') + if line + ) +} + +viewing_key = 'password' + +account_key = 'drpresident' if chain_config['chain-id'] == 'holodeck-2' else 'a' +backend = None if chain_config['chain-id'] == 'holodeck-2' else 'test' +account = run_command(['secretcli', 'keys', 'show', '-a', account_key]).rstrip() + +print('ACCOUNT', account) + +print('Configuring sSCRT') +sscrt = SNIP20(gen_label(8), + name='secretSCRT', symbol='SSCRT', + decimals=6, public_total_supply=True, + enable_deposit=True, enable_burn=True, + enable_redeem=True, admin=account_key, + uploader=account_key, backend=backend) +print(sscrt.address) +sscrt.execute({'set_viewing_key': {'key': viewing_key}}) + +deposit_amount = '100000000uscrt' +print('Depositing', deposit_amount) +sscrt.execute({'deposit': {}}, account, deposit_amount) + +init = { + 'admin': account, + 'treasury': account, + 'sscrt': { + 'address': sscrt.address, + 'code_hash': sscrt.code_hash, + }, + 'viewing_key': viewing_key, +} + +scrt_staking = Contract( + '../compiled/scrt_staking.wasm.gz', + json.dumps(init), + gen_label(8), +) + +#print('get_config') +#print(scrt_staking.query({'get_config': {}})) +print('Sending 100000000 usscrt for staking') +print(sscrt.execute({ + "send": { + "recipient": scrt_staking.address, + "amount": str(100000000), + }, + }, + account, +)) + + +print('BALANCES') +print(sscrt.query({'balance': {'address': scrt_staking.address, 'key': viewing_key}})) +print(run_command(['secretcli', 'q', 'account', scrt_staking.address])) + +delegations = scrt_staking.query({'delegations': {}}) + +print('DELEGATIONS') +for delegation in delegations: + print(scrt_staking.query({'delegation': {'validator': delegation['validator']}})) + +print('UNBONDING') +for delegation in delegations: + print(scrt_staking.execute({'unbond': {'validator': delegation['validator']}})) + +print('CLAIMING') +for delegation in delegations: + print(scrt_staking.execute({'claim': {'validator': delegation['validator']}})) + + +delegations = scrt_staking.query({'delegations': {}}) + +print('DELEGATIONS') +for delegation in delegations: + print(scrt_staking.query({'delegation': {'validator': delegation['validator']}})) diff --git a/tools/doc2book/Cargo.toml b/tools/doc2book/Cargo.toml new file mode 100644 index 000000000..8d02b99fe --- /dev/null +++ b/tools/doc2book/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "doc2book" +version = "0.1.0" +edition = "2018" + +[[bin]] +name = "doc2book" +path = "src/doc2book.rs" + +[dev-dependencies] +pretty_assertions = "1.0.0" diff --git a/tools/doc2book/README.md b/tools/doc2book/README.md new file mode 100644 index 000000000..d4bff1378 --- /dev/null +++ b/tools/doc2book/README.md @@ -0,0 +1,59 @@ +# `doc2book` +A simple tool that scrapes rustdoc comments, `doc2book`-only comments and specified Rust code from a crate's source files +and outputs them as a single file (e.g. a Markdown book chapter). + +## Usage +The usage is simple, specify the crate and the output file path +``` +USAGE: doc2book CRATE_DIR OUT_FILE_PATH +``` + +Example from Shade workspace root directory: +``` +cargo r -p doc2book --release -- packages/shade_protocol doc/book/src/smart_contracts.md +``` + +### Comments +The following comments are scraped: +- `//! `: [rustdoc][1] crate-level (inner) doc comments +- `/// `: [rustdoc][1] code-level (outer) doc comments +- `//# `: `doc2book` comments, these will be ignored by rustdoc but picked up by `doc2book` + +[1]: https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html + +### Code +The comments `// book_include_code` and `// end_book_include_code` mark a section of code to be copied verbatim and place in a Rust code block (Markdown syntax). +This code: + +```rust +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +// book_include_code +pub struct Contract { + /// The address of the contract + pub address: HumanAddr, + /// The hex encoded hash of the contract code + pub code_hash: String, +} +// end_book_include_code +``` + +Would result in this Markdown section: + +~~~ +```rust +pub struct Contract { + /// The address of the contract + pub address: HumanAddr, + /// The hex encoded hash of the contract code + pub code_hash: String, +} +``` +~~~ + +## Limitations +As the tool is currently very simple it has a few limitations: +- The crate structure is expected to be one file per module at the same directory level as the lib.rs file. +- The tool outputs everything as a single file, it could be made to create a 'sub-chapter' file for each module. +- Due to it creating a single file, the ordering of comments, modules declarations and the code is the order it will appear output file. +- Adding whitespace between scraped sections needs to be explicit, i.e. an empty comment line needs to be inserted where you want a blank line `//! `. diff --git a/tools/doc2book/src/doc2book.rs b/tools/doc2book/src/doc2book.rs new file mode 100644 index 000000000..c9477c3ca --- /dev/null +++ b/tools/doc2book/src/doc2book.rs @@ -0,0 +1,326 @@ +use std::{ + fs::File, + io::{BufRead, BufReader, Error as IoError, Lines, Write}, +}; + +const USAGE: &str = "USAGE: doc2book CRATE_DIR OUT_FILE_PATH"; + +type Result = std::result::Result; + +#[derive(Debug)] +enum Error { + CrateSourceDirNotFound(String), + LibRsNotFound(String), + ModuleSourceNotFound(String), + Io(IoError), +} + +impl From for Error { + fn from(err: IoError) -> Self { + Self::Io(err) + } +} + +// source reading interface +trait ReadSource { + type Src: BufRead; + + fn lib_rs_path(&self) -> &str; + + fn module_path_from_name(&self, module: &str) -> String; + + fn lines_from_path(&self, src_path: &str) -> Result>; +} + +// source reading implementor for crates +struct CrateSource { + crate_src_dir: String, + lib_rs_path: String, +} + +impl CrateSource { + // ensure the crate is valid, + // i.e. it's src directory exists and contains a lib.rs file + fn new(crate_dir: &str) -> Result { + let crate_src_dir = format!("{}/src", crate_dir); + let lib_rs_path = format!("{}/lib.rs", crate_src_dir); + + if std::fs::metadata(&crate_src_dir).is_err() { + return Err(Error::CrateSourceDirNotFound(crate_src_dir)); + } + + if std::fs::metadata(&lib_rs_path).is_err() { + return Err(Error::LibRsNotFound(lib_rs_path)); + } + + Ok(CrateSource { + crate_src_dir, + lib_rs_path, + }) + } +} + +impl ReadSource for CrateSource { + type Src = BufReader; + + fn lib_rs_path(&self) -> &str { + &self.lib_rs_path + } + + fn module_path_from_name(&self, module: &str) -> String { + format!("{}/{}.rs", self.crate_src_dir, module) + } + + fn lines_from_path(&self, src_path: &str) -> Result> { + if std::fs::metadata(src_path).is_err() { + return Err(Error::ModuleSourceNotFound(src_path.to_string())); + } + + let file = File::open(src_path)?; + let lines = BufReader::new(file).lines(); + + Ok(lines) + } +} + +struct Output { + output: Vec, +} + +impl Output { + fn new() -> Output { + // allocate a decent chunk of memory at the start + let output = Vec::with_capacity(1_000_000_000); + Output { output } + } + + fn push_line(&mut self, line: &str) { + writeln!(&mut self.output, "{}", line).unwrap(); + } + + fn write_into(&self, w: &mut dyn Write) -> Result<()> { + w.write_all(&self.output)?; + Ok(()) + } +} + +fn parse_args() -> Option<(String, String)> { + let mut args = std::env::args().skip(1); + let crate_dir = args.next()?; + let out_path = args.next()?; + Some((crate_dir, out_path)) +} + +fn find_doc_comment(line: &str) -> Option<&str> { + line.strip_prefix("//! ") + .or_else(|| line.strip_prefix("//# ")) + .or_else(|| line.strip_prefix("/// ")) +} + +// scrape from code between pragmas and each type of doc comment +fn process_module(input: &I, output: &mut Output, module_name: &str) -> Result<()> +where + I: ReadSource, +{ + let module_path = input.module_path_from_name(module_name); + eprintln!("Processing module file: {}", module_path); + + let mut code_transcribe_enabled = false; + + for line in input.lines_from_path(&module_path)? { + let line = line?; + + if line.starts_with("// book_include_code") { + output.push_line("```rust"); + code_transcribe_enabled = true; + continue; + } + + if line.starts_with("// end_book_include_code") { + output.push_line("```"); + code_transcribe_enabled = false; + continue; + } + + if code_transcribe_enabled { + output.push_line(&line); + continue; + } + + if let Some(line) = find_doc_comment(&line) { + output.push_line(line); + } + } + + Ok(()) +} + +// start with the crate root, lib.rs +// scrape code comments and with each public module inserted in the order they appear +fn process_crate(input: I, output: &mut Output) -> Result<()> +where + I: ReadSource, +{ + let lib_rs_path = input.lib_rs_path(); + eprintln!("Processing lib.rs file: {}", lib_rs_path); + + for line in input.lines_from_path(lib_rs_path)? { + let line = line?; + + if let Some(doc) = find_doc_comment(&line) { + output.push_line(doc); + continue; + } + + if line.starts_with("pub mod ") && line.ends_with(';') { + let module = line + .strip_prefix("pub mod ") + .unwrap() + .strip_suffix(';') + .unwrap(); + + process_module(&input, output, module)?; + } + } + + Ok(()) +} + +fn main() { + let (crate_root_dir, out_path) = match parse_args() { + Some(args) => args, + _ => return eprintln!("{}", USAGE), + }; + + let input = match CrateSource::new(&crate_root_dir) { + Ok(input) => input, + Err(Error::CrateSourceDirNotFound(path)) => { + eprintln!( + "{} does not exist. Make sure the specified directory is a Rust project.", + path + ); + return eprintln!("{}", USAGE); + } + Err(Error::LibRsNotFound(path)) => { + eprintln!("{} not does not exist. The crate must be a library.", path); + return eprintln!("{}", USAGE); + } + Err(err) => panic!("unexpected err: {:?}", err), + }; + + let mut output = Output::new(); + + let result = process_crate(input, &mut output).and_then(|_| { + let mut out_file = File::create(out_path)?; + output.write_into(&mut out_file) + }); + + if let Err(err) = result { + match err { + Error::ModuleSourceNotFound(path) => { + eprintln!( + "Processing module file {} failed: file does not exist", + path + ) + } + Error::Io(err) => eprintln!("Unexpected I/O Error: {}", err), + err => panic!("unexpected err: {:?}", err), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + const LIB_RS: &str = r#"//! # Main Title +//! Some text +//! + +pub mod helper_mod; + +//# ## Common Types +//# Some more text +//# + +pub mod common_types; +"#; + + const COMMON_TYPES_RS: &str = r#"use some_dep::module; +use some_other_dep::module; + +//# ### `Contract` +/// Represents another contract on the network +/// + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +// book_include_code +pub struct Contract { + /// The address of the contract + pub address: HumanAddr, + /// The hex encoded hash of the contract code + pub code_hash: String, +} +// end_book_include_code"#; + + const EXPECTED_OUTPUT: &str = r#"# Main Title +Some text + +## Common Types +Some more text + +### `Contract` +Represents another contract on the network + +```rust +pub struct Contract { + /// The address of the contract + pub address: HumanAddr, + /// The hex encoded hash of the contract code + pub code_hash: String, +} +``` +"#; + + struct TestInput; + + impl ReadSource for TestInput { + type Src = BufReader<&'static [u8]>; + + fn lib_rs_path(&self) -> &str { + "src/lib.rs" + } + + fn module_path_from_name(&self, module: &str) -> String { + format!("src/{}.rs", module) + } + + fn lines_from_path(&self, src_path: &str) -> Result> { + let s: &'static str = match src_path { + "src/lib.rs" => LIB_RS, + "src/common_types.rs" => COMMON_TYPES_RS, + "src/helper_mod.rs" => "", + _ => panic!("Unexpected path: {}", src_path), + }; + + Ok(BufReader::new(s.as_bytes()).lines()) + } + } + + #[test] + fn it_works() { + let mut output = Output::new(); + + process_crate(TestInput, &mut output).unwrap(); + + let mut actual = Vec::new(); + + output.write_into(&mut actual).unwrap(); + + let actual = String::from_utf8(actual).unwrap(); + + assert_eq!(actual, EXPECTED_OUTPUT.to_owned()) + } +}