diff --git a/Cargo.lock b/Cargo.lock index d66b8a9..0cdbaa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aead" @@ -23,6 +23,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "base16ct" version = "0.1.1" @@ -81,6 +102,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -89,9 +116,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.99" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" [[package]] name = "cfg-if" @@ -123,6 +150,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "cipher" version = "0.4.4" @@ -146,6 +187,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cosmwasm-derive" version = "1.5.5" @@ -414,6 +461,29 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "inout" version = "0.1.3" @@ -429,6 +499,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "k256" version = "0.11.6" @@ -448,33 +528,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] -name = "minicbor" -version = "0.18.0" +name = "log" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a20020e8e2d1881d8736f64011bb5ff99f1db9947ce3089706945c8915695cb" -dependencies = [ - "minicbor-derive", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] -name = "minicbor-derive" -version = "0.12.0" +name = "minicbor" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8608fb1c805b5b6b3d5ab7bd95c40c396df622b64d77b2d621a5eae1eed050ee" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "c0452a60c1863c1f50b5f77cd295e8d2786849f35883f0b9e18e7e6e1b5691b0" [[package]] -name = "minicbor-ser" -version = "0.2.0" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0834b86a9c56311671913d56f640d7f0b6da803df61121661cc890f0edc0eb1" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "minicbor", - "serde", + "autocfg", ] [[package]] @@ -523,7 +594,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", - "uint", + "uint 0.9.5", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "uint 0.10.0", ] [[package]] @@ -700,7 +781,7 @@ dependencies = [ "serde", "serde-json-wasm", "thiserror", - "uint", + "uint 0.9.5", ] [[package]] @@ -715,8 +796,8 @@ dependencies = [ [[package]] name = "secret-toolkit" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ "secret-toolkit-crypto", "secret-toolkit-notification", @@ -729,9 +810,10 @@ dependencies = [ [[package]] name = "secret-toolkit-crypto" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ + "cc", "hkdf", "rand_chacha", "rand_core 0.6.4", @@ -742,14 +824,15 @@ dependencies = [ [[package]] name = "secret-toolkit-notification" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ "chacha20poly1305", "generic-array", + "hex", "hkdf", - "minicbor-ser", - "primitive-types", + "minicbor", + "primitive-types 0.12.2", "ripemd", "schemars", "secret-cosmwasm-std", @@ -760,8 +843,8 @@ dependencies = [ [[package]] name = "secret-toolkit-permit" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ "bech32", "remain", @@ -769,13 +852,15 @@ dependencies = [ "schemars", "secret-cosmwasm-std", "secret-toolkit-crypto", + "secret-toolkit-storage", + "secret-toolkit-utils", "serde", ] [[package]] name = "secret-toolkit-serialization" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ "bincode2", "schemars", @@ -785,8 +870,8 @@ dependencies = [ [[package]] name = "secret-toolkit-storage" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ "secret-cosmwasm-std", "secret-cosmwasm-storage", @@ -796,9 +881,10 @@ dependencies = [ [[package]] name = "secret-toolkit-utils" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ + "chrono", "schemars", "secret-cosmwasm-std", "secret-cosmwasm-storage", @@ -807,8 +893,8 @@ dependencies = [ [[package]] name = "secret-toolkit-viewing-key" -version = "0.10.0" -source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=8aed92d589dc119f69d20f8538d5a6eea8003d95#8aed92d589dc119f69d20f8538d5a6eea8003d95" +version = "0.10.3" +source = "git+https://github.com/SolarRepublic/secret-toolkit.git?rev=4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4#4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" dependencies = [ "base64 0.21.7", "schemars", @@ -921,9 +1007,12 @@ dependencies = [ "base64 0.21.7", "constant_time_eq", "cosmwasm-schema", - "minicbor-ser", - "primitive-types", + "hex", + "minicbor", + "primitive-types 0.13.1", "rand", + "rand_chacha", + "rand_core 0.6.4", "schemars", "secret-cosmwasm-std", "secret-cosmwasm-storage", @@ -1016,6 +1105,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1044,6 +1145,133 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.66", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index b63d7f2..d939539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,19 +37,22 @@ gas_evaporation = [] cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11" } cosmwasm-storage = { package = "secret-cosmwasm-storage", version = "1.1.11" } rand = { version = "0.8.5", default-features = false } -# secret-toolkit = { version = "0.10.0", default-features = false, features = ["permit", "storage", "viewing-key"] } -secret-toolkit = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["permit", "storage", "viewing-key", "notification"], rev = "8aed92d589dc119f69d20f8538d5a6eea8003d95" } -# secret-toolkit-crypto = { version = "0.10.0", default-features = false, features = ["hash"] } -secret-toolkit-crypto = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["hash"], rev = "8aed92d589dc119f69d20f8538d5a6eea8003d95" } +# secret-toolkit = { version = "0.10.2", default-features = false, features = ["permit", "storage", "viewing-key", "notification"] } +secret-toolkit = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["permit", "storage", "viewing-key", "notification"], rev = "4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" } +# secret-toolkit-crypto = { version = "0.10.2", default-features = false, features = ["hash", "hkdf", "rand"] } +secret-toolkit-crypto = { git = "https://github.com/SolarRepublic/secret-toolkit.git", default-features = false, features = ["hash", "hkdf", "rand"], rev = "4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4" } static_assertions = "1.1.0" +rand_core = { version = "0.6.4", default-features = false } +rand_chacha = { version = "0.3.1", default-features = false } schemars = "0.8.12" serde = { version = "1.0.158", default-features = false, features = ["derive"] } serde-big-array = "0.5.1" base64 = "0.21.0" constant_time_eq = "0.3.0" -primitive-types = { version = "0.12.2", default-features = false } -minicbor-ser = "0.2.0" +primitive-types = { version = "0.13.1", default-features = false } +minicbor = "0.25.1" +hex = "0.4.3" [dev-dependencies] cosmwasm-schema = { version = "1.1.8" } diff --git a/README.md b/README.md index 294b59d..d04670b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SNIP-20 Reference Implementation -This is an implementation of a [SNIP-20](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md), [SNIP-21](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-21.md), [SNIP-22](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-22.md), [SNIP-23](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-23.md), [SNIP-24](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md), [SNIP-25](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-25.md) and [SNIP-26](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-26.md) compliant token contract. +This is an implementation of a [SNIP-20](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md), [SNIP-21](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-21.md), [SNIP-22](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-22.md), [SNIP-23](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-23.md), [SNIP-24](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md), [~~SNIP-25~~](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-25.md), [SNIP-26](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-26.md), [~~SNIP-50~~](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-50.md) and [SNIP-52](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-52.md) compliant token contract. > **Note:** > The master branch contains new features not covered by officially-released SNIPs and may be subject to change. When releasing a token on mainnet, we recommend you start with a [tagged release](https://github.com/scrtlabs/snip20-reference-impl/tags) to ensure compatibility with SNIP standards. @@ -66,112 +66,27 @@ All transactions are encrypted, so if you want to see the error returned by a fa `secretcli q compute tx ` -# SNIP 25 Security Update - -## Security Changes -1. Implemented the ability to have decoy addresses for every operation that access account's balance -2. Converted every add operation related to account's balance and total supply -3. Started using u128 instead of Uint128 - -## Decoys -### Transaction That Support Decoys -1. Redeem -2. Deposit -3. Transfer -4. TransferFrom -5. Send -6. SendFrom -7. Burn -8. BurnFrom -9. Mint -10. BatchTransfer - For every action (The strength of the decoys will be the minimal strength of all of the actions) -11. BatchSend - For every action (The strength of the decoys will be the minimal strength of all of the actions) -12. BatchTransferFrom - For every action (The strength of the decoys will be the minimal strength of all of the actions) -13. BatchSendFrom - For every action (The strength of the decoys will be the minimal strength of all of the actions) -14. BatchMint - For every action (The strength of the decoys will be the minimal strength of all of the actions) -15. BatchBurnFrom - For every action (The strength of the decoys will be the minimal strength of all of the actions) - -### Example -```secretcli tx compute execute '{"transfer":{"recipient":"
","amount":"", "entropy":"", "decoys":<[addresses_list]>}}' --from ``` - -## Future Work -| Topic | Immediate-term solution | Medium-term solution | Long-term solution | -| --- | --- | --- | --- | -| Receiver privacy | Decoys - offer limited privacy, since it depends a lot on how you choose decoys. There’s probably no way to select decoys effectively enough, and thus it only makes it a bit harder but effectively doesn’t provide receiver privacy to a sophisticated long-term attacker | Some sort of bucketing? - still no clear path forward| ORAM? - still no clear path forward | -| Transfer amount privacy - subtractions (Transfer/Send/Burn) | None | None | Merkle proofs for storage reads - will make it very difficult to simulate transactions and play with storage. | - -# SNIP 25 Other Updates - -## All Allowances -Adds the ability for an owner to query for all allowances they have given out, as well as for a spender to query for all allowances they have received. - -## Queries - -### AllowancesGiven - -This query MUST be authenticated. - -Returns the list of allowances given out by the current account as an owner, as well as the total count of allowances given out. - -Results SHOULD be paginated. Results MUST be sorted in reverse chronological order by the datetime at which the allowance was first created (i.e., order is not determined by expiration, nor by last modified). - -#### Request - -| Name | Type | Description | optional | -| ---- | ---- | ----------- | -------- | -| [with_permit].query.allowances_given.owner | string | Account from which tokens are allowed to be taken | no | -| [with_permit].query.allowances_given.page_size | number | Number of allowances to return, starting from the latest. i.e. n=1 will return only the latest allowance | no | -| [with_permit].query.allowances_given.page | number | Defaults to 0. Specifying a positive number will skip page * page_size txs from the start. | yes | - -#### Response -```json -{ - "allowances_given": { - "owner": "
", - "allowances": [ - { - "spender": "
", - "allowance": "Uint128", - "expiration": 1234, - }, - { "...": "..." } - ], - "count": 200 - } -} -``` - -### AllowancesReceived - -This query MUST be authenticated. - -Returns the list of allowances given to the current account as a spender, as well as the total count of allowances received. - -Results SHOULD be paginated. Results MUST be sorted in reverse chronological order by the datetime at which the allowance was first created (i.e., order is not determined by expiration). - -#### Request - -| Name | Type | Description | optional | -| ---- | ---- | ----------- | -------- | -| [with_permit.]query.allowances_received.spender | string | Account which is allowed to spend tokens on behalf of the owner | no | -| [with_permit.]query.allowances_received.page_size | number | Number of allowances to return, starting from the latest. i.e. n=1 will return only the latest allowance | no | -| [with_permit.]query.allowances_received.page | number | Defaults to 0. Specifying a positive number will skip page * page_size txs from the start. | yes | - -#### Response - -```json -{ - "allowances_received": { - "spender": "
", - "allowances": [ - { - "owner": "
", - "allowance": "Uint128", - "expiration": 1234, - }, - { "...": "..." } - ], - "count": 200 - } -} -``` +## Privacy Enhancements + + - All transfers/sends (including batch and *_from) use the delayed write buffer (dwb) to address "spicy printf" storage access pattern attacks. + - Additionally, a bitwise trie of bucketed entries (dwb) creates dynamic anonymity sets for senders/owners, whose balance must be checked when transferring/sending. It also enhances privacy for recipients. + - When querying for Transaction History, each event's `id` field returned in responses are deterministically obfuscated by `ChaChaRng(XorBytes(ChaChaRng(actual_event_id), internal_secret)) >> (64 - 53)` for better privacy. Without this, an attacker could deduce the number of events that took place between two transactions. + +## SNIP-52: Private Push Notifications + +This contract publishes encrypted messages to the event log which carry data intended to notify recipients of actions that affect them, such as token transfer and allowances. + +Direct channels: + - `recvd` -- emitted to a recipient when their account receives funds via one of `transfer`, `send`, `transfer_from`, or `send_from`. The notification data includes the amount, the sender, and the memo length. + - `spent` -- emitted to an owner when their funds are spent, via one of `transfer`, `send`, `transfer_from` or `send_from`. The notification data includes the amount, the recipient, the owner's new balance, and a few other pieces of information such as memo length, number of actions, and whether the spender was the transaction's sender. + - `allowance` -- emitted to a spender when some allower account has granted them or modified an existing allowance to spend their tokens, via `increase_allowance` or `decrease_allowance`. The notification data includes the amount, the allower, and the expiration of the allowance. + +Group channels: + - `multirecvd` -- emitted to a group of recipients (up to 16) when a `batch_transfer`, `batch_send`, `batch_transfer_from`, or `batch_send_from` has been executed. Each recipient will receive a packet of data containing the amount they received, the last 8 bytes of the owner's address, and some additional metadata. + - `multispent` -- emitted to a group of spenders (up to 16) when a `batch_transfer_from`, or `batch_send_from` has been executed. Each spender will receive a packet of data containing the amount that was spent, the last 8 bytes of the recipient's address, and some additional metadata. + + +## Security Features + + - Transfers to the contract itself will be rejected to prevent accidental loss of funds. + - The migration allows for a one-time processing of refunding any previous transfers made to the contract itself. diff --git a/src/btbe.rs b/src/btbe.rs index c213b44..999fee2 100644 --- a/src/btbe.rs +++ b/src/btbe.rs @@ -4,7 +4,6 @@ include!(concat!(env!("OUT_DIR"), "/config.rs")); use constant_time_eq::constant_time_eq; use cosmwasm_std::{CanonicalAddr, StdError, StdResult, Storage}; -use primitive_types::U256; use secret_toolkit::{ serialization::{Bincode2, Serde}, storage::Item, @@ -13,7 +12,7 @@ use secret_toolkit_crypto::hkdf_sha_256; use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; -use crate::state::{safe_add_u64, INTERNAL_SECRET}; +use crate::{constants::{ADDRESS_BYTES_LEN, IMPOSSIBLE_ADDR}, dwb::constant_time_if_else_u32, state::{safe_add, safe_add_u64, INTERNAL_SECRET_SENSITIVE}}; use crate::dwb::{amount_u64, DelayedWriteBufferEntry, TxBundle}; #[cfg(feature = "gas_tracking")] use crate::gas_tracker::GasTracker; @@ -29,32 +28,18 @@ const BUCKETING_SALT_BYTES: &[u8; 14] = b"bucketing-salt"; const U32_BYTES: usize = 4; const U128_BYTES: usize = 16; -#[cfg(test)] -const BTBE_BUCKET_ADDRESS_BYTES: usize = 54; -#[cfg(not(test))] -const BTBE_BUCKET_ADDRESS_BYTES: usize = 20; -const BTBE_BUCKET_BALANCE_BYTES: usize = 8; // Max 16 (u128) +const BTBE_BUCKET_ADDRESS_BYTES: usize = ADDRESS_BYTES_LEN; +const BTBE_BUCKET_BALANCE_BYTES: usize = 8; // Max 16 (u64) const BTBE_BUCKET_HISTORY_BYTES: usize = 4; // Max 4 (u32) +const BTBE_BUCKET_CACHE_BYTES: usize = 0; const_assert!(BTBE_BUCKET_BALANCE_BYTES <= U128_BYTES); const_assert!(BTBE_BUCKET_HISTORY_BYTES <= U32_BYTES); const BTBE_BUCKET_ENTRY_BYTES: usize = - BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES + BTBE_BUCKET_HISTORY_BYTES; + BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES + BTBE_BUCKET_HISTORY_BYTES + + BTBE_BUCKET_CACHE_BYTES; -/// canonical address bytes corresponding to the 33-byte null public key, in hexadecimal -#[cfg(test)] -const IMPOSSIBLE_ADDR: [u8; BTBE_BUCKET_ADDRESS_BYTES] = [ - 0x29, 0xCF, 0xC6, 0x37, 0x62, 0x55, 0xA7, 0x84, 0x51, 0xEE, 0xB4, 0xB1, 0x29, 0xED, 0x8E, 0xAC, - 0xFF, 0xA2, 0xFE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -]; -#[cfg(not(test))] -const IMPOSSIBLE_ADDR: [u8; BTBE_BUCKET_ADDRESS_BYTES] = [ - 0x29, 0xCF, 0xC6, 0x37, 0x62, 0x55, 0xA7, 0x84, 0x51, 0xEE, 0xB4, 0xB1, 0x29, 0xED, 0x8E, 0xAC, - 0xFF, 0xA2, 0xFE, 0xEF, -]; /// A `StoredEntry` consists of the address, balance, and tx bundle history length in a byte array representation. /// The methods of the struct implementation also handle pushing and getting the tx bundle history in a simplified @@ -156,22 +141,59 @@ impl StoredEntry { Ok(()) } + pub fn save_hash_cache(&mut self, storage: &dyn Storage) -> StdResult<()> { + let hash_bytes = hkdf_sha_256( + &Some(BUCKETING_SALT_BYTES.to_vec()), + INTERNAL_SECRET_SENSITIVE.load(storage)?.as_slice(), + self.address_slice(), + 32, + )?; + + let start = BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES + BTBE_BUCKET_HISTORY_BYTES; + let end = start + BTBE_BUCKET_CACHE_BYTES; + self.0[start..end].copy_from_slice(&hash_bytes.as_slice()[0..BTBE_BUCKET_CACHE_BYTES]); + Ok(()) + } + + pub fn routes_to_right_node(&self, bit_pos: usize, secret: &[u8]) -> StdResult { + // target byte value + let byte; + + // bit pos is cached + if bit_pos < (BTBE_BUCKET_CACHE_BYTES << 3) { + // select the byte from cache corresponding to this bit position + byte = self.0[BTBE_BUCKET_ADDRESS_BYTES + BTBE_BUCKET_BALANCE_BYTES + BTBE_BUCKET_HISTORY_BYTES + (bit_pos >> 3)]; + } + // not cached; calculate on the fly + else { + // create key bytes + let key_bytes = hkdf_sha_256( + &Some(BUCKETING_SALT_BYTES.to_vec()), + secret, + self.address_slice(), + 32, + )?; + + // select the byte containing the target bit + byte = key_bytes[bit_pos >> 3]; + } + + // extract value at bit position and turn into bool + return Ok(((byte >> (7 - (bit_pos % 8))) & 1) != 0); + + } + pub fn merge_dwb_entry( &mut self, storage: &mut dyn Storage, dwb_entry: &DelayedWriteBufferEntry, amount_spent: Option, ) -> StdResult<()> { - let history_len = self.history_len()?; - if history_len == 0 { - return Err(StdError::generic_err( - "use `from` to create new entry from dwb_entry", - )); - } - + // increase account's stored balance let mut balance = self.balance()?; safe_add_u64(&mut balance, dwb_entry.amount()?); + // safety check amount spent before spending from balance let amount_spent = amount_u64(amount_spent)?; // error should never happen because already checked in `settle_sender_or_owner_account` @@ -184,15 +206,48 @@ impl StoredEntry { ))); }; + // set new balance to stored entry self.set_balance(balance)?; - // peek at the last tx bundle added - let last_tx_bundle = self.get_tx_bundle_at(storage, history_len - 1)?; + // retrieve currenty history length + let history_len = self.history_len()?; + + // flag if history is empty + let empty_history = (history_len == 0) as u32; + + // position of last tx bundle to read + let bundle_pos = constant_time_if_else_u32( + empty_history, + 0u32, + history_len.wrapping_sub(1) // constant-time subtraction with underflow + ); + + // peek at the last tx bundle added (read the dummy one if its void) + let last_tx_bundle_result = self.get_tx_bundle_at_unchecked(storage, bundle_pos); + if last_tx_bundle_result.is_err() { + return Err(StdError::generic_err(format!( + "missing tx bundle while merging dwb entry!", + ))); + } + + // unwrap + let last_tx_bundle = last_tx_bundle_result?; + + // calculate the appropriate bundle offset to use + let bundle_offset = constant_time_if_else_u32( + empty_history, + 0u32, + last_tx_bundle.offset + (last_tx_bundle.list_len as u32) + ); + + // create new tx bundle let tx_bundle = TxBundle { head_node: dwb_entry.head_node()?, list_len: dwb_entry.list_len()?, - offset: last_tx_bundle.offset + u32::from(last_tx_bundle.list_len), + offset: bundle_offset, }; + + // add to list self.push_tx_bundle(storage, &tx_bundle)?; Ok(()) @@ -249,7 +304,10 @@ impl StoredEntry { fn push_tx_bundle(&mut self, storage: &mut dyn Storage, bundle: &TxBundle) -> StdResult<()> { let len = self.history_len()?; self.set_tx_bundle_at_unchecked(storage, len, bundle)?; - self.set_history_len(len.saturating_add(1))?; + // if the head node is null, then add this as a ghost bundle that does not contribute to len of list, + // and will be overwritten next time + let len_add = constant_time_if_else_u32((bundle.head_node == 0) as u32, 0, 1); + self.set_history_len(len.saturating_add(len_add))?; Ok(()) } } @@ -378,30 +436,13 @@ impl BitwiseTrieNode { } } -/// Determines whether a given entry belongs in the left node (true) or right node (false) -fn entry_belongs_in_left_node(secret: &[u8], entry: StoredEntry, bit_pos: u8) -> StdResult { - // create key bytes - let key_bytes = hkdf_sha_256( - &Some(BUCKETING_SALT_BYTES.to_vec()), - secret, - entry.address_slice(), - 32, - )?; - - // convert to u258 - let key_u256 = U256::from_big_endian(&key_bytes); - - // extract the bit value at the target bit position - return Ok(U256::from(0) == (key_u256 >> (255 - bit_pos)) & U256::from(1)); -} - /// Locates a btbe node given an address; returns tuple of (node, node_id, bit position) pub fn locate_btbe_node( storage: &dyn Storage, address: &CanonicalAddr, -) -> StdResult<(BitwiseTrieNode, u64, u8)> { +) -> StdResult<(BitwiseTrieNode, u64, usize)> { // load internal contract secret - let secret = INTERNAL_SECRET.load(storage)?; + let secret = INTERNAL_SECRET_SENSITIVE.load(storage)?; let secret = secret.as_slice(); // create key bytes @@ -417,7 +458,9 @@ pub fn locate_btbe_node( let mut node = BTBE_TRIE_NODES .add_suffix(&node_id.to_be_bytes()) .load(storage)?; - let mut bit_pos: u8 = 0; + + // bit position + let mut bit_pos: usize = 0; // while the node has children while node.bucket == 0 { @@ -510,9 +553,9 @@ pub fn stored_tx_count(storage: &dyn Storage, entry: &Option) -> St Ok(0) } -// merges a dwb entry into the current node's bucket +// settles a dwb entry into its appropriate bucket // `amount_spent` is any required subtraction due to being sender of tx -pub fn merge_dwb_entry( +pub fn settle_dwb_entry( storage: &mut dyn Storage, dwb_entry: &DelayedWriteBufferEntry, amount_spent: Option, @@ -521,8 +564,11 @@ pub fn merge_dwb_entry( #[cfg(feature = "gas_tracking")] let mut group1 = tracker.group("#merge_dwb_entry.1"); + // ref the entry's recipient address + let address = &dwb_entry.recipient()?; + // locate the node that the given entry belongs in - let (mut node, mut node_id, mut bit_pos) = locate_btbe_node(storage, &dwb_entry.recipient()?)?; + let (mut node, mut node_id, mut bit_pos) = locate_btbe_node(storage, address)?; // load that node's current bucket let mut bucket = node.bucket(storage)?; @@ -531,7 +577,7 @@ pub fn merge_dwb_entry( let mut bucket_id = node.bucket; // search for an existing entry - if let Some((idx, mut found_entry)) = bucket.constant_time_find_address(&dwb_entry.recipient()?) + if let Some((idx, mut found_entry)) = bucket.constant_time_find_address(address) { // found existing entry // merge amount and history from dwb entry @@ -541,7 +587,7 @@ pub fn merge_dwb_entry( #[cfg(feature = "gas_tracking")] group1.logf(format!( "merged {} into node #{}, bucket #{} at position {} ", - dwb_entry.recipient()?, + address, node_id, bucket_id, idx @@ -549,13 +595,18 @@ pub fn merge_dwb_entry( // save updated bucket to storage node.set_and_save_bucket(storage, bucket)?; - } else { + } + // nothing was stored yet + else { // need to insert new entry // create new stored balance entry - let btbe_entry = StoredEntry::from(storage, &dwb_entry, amount_spent)?; + let mut btbe_entry = StoredEntry::from(storage, &dwb_entry, amount_spent)?; + + // cache the address + btbe_entry.save_hash_cache(storage)?; // load contract's internal secret - let secret = INTERNAL_SECRET.load(storage)?; + let secret = INTERNAL_SECRET_SENSITIVE.load(storage)?; let secret = secret.as_slice(); loop { @@ -584,12 +635,11 @@ pub fn merge_dwb_entry( // each entry for entry in bucket.entries { - // left_bucket.add_entry(&entry); // route entry - if entry_belongs_in_left_node(secret, entry, bit_pos)? { - left_bucket.add_entry(&entry); - } else { + if entry.routes_to_right_node(bit_pos, secret)? { right_bucket.add_entry(&entry); + } else { + left_bucket.add_entry(&entry); } } @@ -663,16 +713,16 @@ pub fn merge_dwb_entry( )); // route entry - if entry_belongs_in_left_node(secret, btbe_entry, bit_pos)? { - node = left; - node_id = left_id; - bucket = left_bucket; - bucket_id = left_bucket_id; - } else { + if btbe_entry.routes_to_right_node(bit_pos, secret)? { node = right; node_id = right_id; bucket = right_bucket; bucket_id = right_bucket_id; + } else { + node = left; + node_id = left_id; + bucket = left_bucket; + bucket_id = left_bucket_id; } // increment bit position for next iteration of the loop @@ -760,6 +810,7 @@ mod tests { address: "bob".to_string(), amount: Uint128::new(5000), }]); + assert!( init_result.is_ok(), "Init failed: {}", @@ -767,11 +818,13 @@ mod tests { ); let _env = mock_env(); let _info = mock_info("bob", &[]); + let storage = &mut deps.storage; let canonical = deps .api .addr_canonicalize(Addr::unchecked("bob".to_string()).as_str()) .unwrap(); + let entry = StoredEntry::new(&canonical).unwrap(); assert_eq!(entry.address().unwrap(), canonical); assert_eq!(entry.balance().unwrap(), 0_u64); @@ -799,12 +852,13 @@ mod tests { "Init failed: {}", init_result.err().unwrap() ); - let _env = mock_env(); + let env = mock_env(); let _info = mock_info("bob", &[]); + let storage = &mut deps.storage; - let _ = initialize_btbe(&mut deps.storage).unwrap(); + let _ = initialize_btbe(storage).unwrap(); - let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(&deps.storage).unwrap(); + let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(storage).unwrap(); assert_eq!(btbe_node_count, 1); for i in 1..=128 { @@ -812,18 +866,21 @@ mod tests { .api .addr_canonicalize(Addr::unchecked(format!("{i}zzzzzz")).as_str()) .unwrap(); - let entry = StoredEntry::new(&canonical).unwrap(); + + let mut entry = StoredEntry::new(&canonical).unwrap(); + entry.save_hash_cache(storage).unwrap(); + assert_eq!(entry.address().unwrap(), canonical); assert_eq!(entry.balance().unwrap(), 0_u64); - let dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); + let mut dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); - let _result = merge_dwb_entry(&mut deps.storage, &dwb_entry, None); + let _result = settle_dwb_entry(storage, &mut dwb_entry, None); - let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(&deps.storage).unwrap(); + let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(storage).unwrap(); assert_eq!(btbe_node_count, 1); - let (node, node_id, bit_pos) = locate_btbe_node(&deps.storage, &canonical).unwrap(); + let (node, node_id, bit_pos) = locate_btbe_node(storage, &canonical).unwrap(); assert_eq!( node, BitwiseTrieNode { @@ -841,13 +898,14 @@ mod tests { .api .addr_canonicalize(Addr::unchecked(format!("bob")).as_str()) .unwrap(); - let entry = StoredEntry::new(&canonical).unwrap(); + let mut entry = StoredEntry::new(&canonical).unwrap(); + entry.save_hash_cache(storage); assert_eq!(entry.address().unwrap(), canonical); assert_eq!(entry.balance().unwrap(), 0_u64); - let dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); + let mut dwb_entry = DelayedWriteBufferEntry::new(&canonical).unwrap(); - let _result = merge_dwb_entry(&mut deps.storage, &dwb_entry, None); + let _result = settle_dwb_entry(&mut deps.storage, &mut dwb_entry, None); let btbe_node_count = BTBE_TRIE_NODES_COUNT.load(&deps.storage).unwrap(); assert_eq!(btbe_node_count, 3); diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..0f1bde6 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +pub const ADDRESS_BYTES_LEN: usize = 54; +#[cfg(not(test))] +pub const ADDRESS_BYTES_LEN: usize = 20; + +/// canonical address bytes corresponding to the 33-byte null public key, in hexadecimal +#[cfg(test)] +pub const IMPOSSIBLE_ADDR: [u8; ADDRESS_BYTES_LEN] = [ + 0x29, 0xCF, 0xC6, 0x37, 0x62, 0x55, 0xA7, 0x84, 0x51, 0xEE, 0xB4, 0xB1, 0x29, 0xED, 0x8E, 0xAC, + 0xFF, 0xA2, 0xFE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; +#[cfg(not(test))] +pub const IMPOSSIBLE_ADDR: [u8; ADDRESS_BYTES_LEN] = [ + 0x29, 0xCF, 0xC6, 0x37, 0x62, 0x55, 0xA7, 0x84, 0x51, 0xEE, 0xB4, 0xB1, 0x29, 0xED, 0x8E, 0xAC, + 0xFF, 0xA2, 0xFE, 0xEF, +]; \ No newline at end of file diff --git a/src/contract.rs b/src/contract.rs index 29773f1..9edebae 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -1,49 +1,43 @@ /// This contract implements SNIP-20 standard: /// https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md use cosmwasm_std::{ - entry_point, to_binary, Addr, BankMsg, Binary, BlockInfo, CanonicalAddr, Coin, CosmosMsg, - Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128, Uint64, + entry_point, to_binary, Binary, + Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, }; #[cfg(feature = "gas_evaporation")] use cosmwasm_std::Api; -use secret_toolkit::notification::{get_seed, notification_id, BloomParameters, ChannelInfoData, Descriptor, FlatDescriptor, Notification, NotificationData, StructDescriptor,}; -use secret_toolkit::permit::{Permit, RevokedPermits, TokenPermissions}; +use secret_toolkit::notification::{GroupChannel, DirectChannel,}; +use secret_toolkit::permit::{Permit, TokenPermissions}; use secret_toolkit::utils::{pad_handle_result, pad_query_result}; use secret_toolkit::viewing_key::{ViewingKey, ViewingKeyStore}; use secret_toolkit_crypto::{hkdf_sha_256, sha_256, ContractPrng}; -use crate::batch; +use crate::{execute, execute_admin, execute_deposit_redeem, execute_mint_burn, execute_transfer_send, query}; #[cfg(feature = "gas_tracking")] use crate::dwb::log_dwb; -use crate::dwb::{DelayedWriteBuffer, DWB, TX_NODES}; +use crate::dwb::{DelayedWriteBuffer, DWB}; + +use crate::btbe::initialize_btbe; -use crate::btbe::{ - find_start_bundle, initialize_btbe, stored_balance, stored_entry, stored_tx_count, -}; #[cfg(feature = "gas_tracking")] use crate::gas_tracker::{GasTracker, LoggingExt}; #[cfg(feature = "gas_evaporation")] use crate::msg::Evaporator; use crate::msg::{ - AllowanceGivenResult, AllowanceReceivedResult, ContractStatusLevel, ExecuteAnswer, ExecuteMsg, - InstantiateMsg, QueryAnswer, QueryMsg, QueryWithPermit, ResponseStatus::Success, + ContractStatusLevel, ExecuteMsg, InstantiateMsg, QueryAnswer, QueryMsg, QueryWithPermit, +}; +use crate::notifications::{ + AllowanceNotification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, SpentNotification }; -use crate::notifications::{multi_received_data, multi_spent_data, AllowanceNotificationData, ReceivedNotificationData, SpentNotificationData, MULTI_RECEIVED_CHANNEL_BLOOM_K, MULTI_RECEIVED_CHANNEL_BLOOM_N, MULTI_RECEIVED_CHANNEL_ID, MULTI_RECEIVED_CHANNEL_PACKET_SIZE, MULTI_SPENT_CHANNEL_BLOOM_K, MULTI_SPENT_CHANNEL_BLOOM_N, MULTI_SPENT_CHANNEL_ID, MULTI_SPENT_CHANNEL_PACKET_SIZE}; -use crate::receiver::Snip20ReceiveMsg; use crate::state::{ - safe_add, AllowancesStore, Config, MintersStore, ReceiverHashStore, CHANNELS, CONFIG, CONTRACT_STATUS, INTERNAL_SECRET, TOTAL_SUPPLY + Config, MintersStore, CHANNELS, CONFIG, CONTRACT_STATUS, INTERNAL_SECRET_RELAXED, INTERNAL_SECRET_SENSITIVE, TOTAL_SUPPLY }; use crate::strings::TRANSFER_HISTORY_UNSUPPORTED_MSG; -use crate::transaction_history::{ - store_burn_action, store_deposit_action, store_mint_action, store_redeem_action, - store_transfer_action, Tx, -}; /// We make sure that responses from `handle` are padded to a multiple of this size. pub const RESPONSE_BLOCK_SIZE: usize = 256; -pub const NOTIFICATION_BLOCK_SIZE: usize = 36; -pub const PREFIX_REVOKED_PERMITS: &str = "revoked_permits"; +pub const NOTIFICATION_BLOCK_SIZE: usize = 1; #[entry_point] pub fn instantiate( @@ -95,23 +89,31 @@ pub fn instantiate( rng_entropy.extend_from_slice(info.sender.as_bytes()); rng_entropy.extend_from_slice(entropy); - // Create INTERNAL_SECRET + // create internal secrets let salt = Some(sha_256(&rng_entropy).to_vec()); - let internal_secret = hkdf_sha_256( + let internal_secret_sensitive = hkdf_sha_256( + &salt, + rng_seed.0.as_slice(), + "contract_internal_secret_sensitive".as_bytes(), + 32, + )?; + INTERNAL_SECRET_SENSITIVE.save(deps.storage, &internal_secret_sensitive)?; + + let internal_secret_relaxed = hkdf_sha_256( &salt, rng_seed.0.as_slice(), - "contract_internal_secret".as_bytes(), + "contract_internal_secret_relaxed".as_bytes(), 32, )?; - INTERNAL_SECRET.save(deps.storage, &internal_secret)?; + INTERNAL_SECRET_RELAXED.save(deps.storage, &internal_secret_relaxed)?; // Hard-coded channels let channels: Vec = vec![ - ReceivedNotificationData::CHANNEL_ID.to_string(), - SpentNotificationData::CHANNEL_ID.to_string(), - AllowanceNotificationData::CHANNEL_ID.to_string(), - MULTI_RECEIVED_CHANNEL_ID.to_string(), - MULTI_SPENT_CHANNEL_ID.to_string(), + RecvdNotification::CHANNEL_ID.to_string(), + SpentNotification::CHANNEL_ID.to_string(), + AllowanceNotification::CHANNEL_ID.to_string(), + MultiRecvdNotification::CHANNEL_ID.to_string(), + MultiSpentNotification::CHANNEL_ID.to_string(), ]; for channel in channels { @@ -124,7 +126,7 @@ pub fn instantiate( let balance_address = deps.api.addr_canonicalize(balance.address.as_str())?; #[cfg(feature = "gas_tracking")] let mut tracker = GasTracker::new(deps.api); - perform_mint( + execute_mint_burn::perform_mint( deps.storage, &mut rng, &raw_admin, @@ -200,12 +202,22 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S ContractStatusLevel::StopAll | ContractStatusLevel::StopAllButRedeems => { let response = match msg { ExecuteMsg::SetContractStatus { level, .. } => { - set_contract_status(deps, info, level) + // load contract config from storage + let config = CONFIG.load(deps.storage)?; + + // check that message sender is the admin + if config.admin != info.sender { + return Err(StdError::generic_err( + "This is an admin command. Admin commands can only be run from admin address", + )); + } + + execute_admin::set_contract_status(deps, level) } ExecuteMsg::Redeem { amount, denom, .. } if contract_status == ContractStatusLevel::StopAllButRedeems => { - try_redeem(deps, env, info, amount, denom) + execute_deposit_redeem::try_redeem(deps, env, info, amount, denom) } _ => Err(StdError::generic_err( "This contract is stopped and this action is not allowed", @@ -218,8 +230,8 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S let response = match msg.clone() { // Native - ExecuteMsg::Deposit { .. } => try_deposit(deps, env, info, &mut rng), - ExecuteMsg::Redeem { amount, denom, .. } => try_redeem(deps, env, info, amount, denom), + ExecuteMsg::Deposit { .. } => execute_deposit_redeem::try_deposit(deps, env, info, &mut rng), + ExecuteMsg::Redeem { amount, denom, .. } => execute_deposit_redeem::try_redeem(deps, env, info, amount, denom), // Base ExecuteMsg::Transfer { @@ -227,7 +239,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S amount, memo, .. - } => try_transfer(deps, env, info, &mut rng, recipient, amount, memo), + } => execute_transfer_send::try_transfer(deps, env, info, &mut rng, recipient, amount, memo), ExecuteMsg::Send { recipient, recipient_code_hash, @@ -235,7 +247,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S msg, memo, .. - } => try_send( + } => execute_transfer_send::try_send( deps, env, info, @@ -247,15 +259,15 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S msg, ), ExecuteMsg::BatchTransfer { actions, .. } => { - try_batch_transfer(deps, env, info, &mut rng, actions) + execute_transfer_send::try_batch_transfer(deps, env, info, &mut rng, actions) } - ExecuteMsg::BatchSend { actions, .. } => try_batch_send(deps, env, info, &mut rng, actions), - ExecuteMsg::Burn { amount, memo, .. } => try_burn(deps, env, info, amount, memo), + ExecuteMsg::BatchSend { actions, .. } => execute_transfer_send::try_batch_send(deps, env, info, &mut rng, actions), + ExecuteMsg::Burn { amount, memo, .. } => execute_mint_burn::try_burn(deps, env, info, amount, memo), ExecuteMsg::RegisterReceive { code_hash, .. } => { - try_register_receive(deps, info, code_hash) + execute::try_register_receive(deps, info, code_hash) } - ExecuteMsg::CreateViewingKey { entropy, .. } => try_create_key(deps, env, info, entropy, &mut rng), - ExecuteMsg::SetViewingKey { key, .. } => try_set_key(deps, info, key), + ExecuteMsg::CreateViewingKey { entropy, .. } => execute::try_create_key(deps, env, info, entropy, &mut rng), + ExecuteMsg::SetViewingKey { key, .. } => execute::try_set_key(deps, info, key), // Allowance ExecuteMsg::IncreaseAllowance { @@ -263,20 +275,20 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S amount, expiration, .. - } => try_increase_allowance(deps, env, info, spender, amount, expiration), + } => execute::try_increase_allowance(deps, env, info, spender, amount, expiration), ExecuteMsg::DecreaseAllowance { spender, amount, expiration, .. - } => try_decrease_allowance(deps, env, info, spender, amount, expiration), + } => execute::try_decrease_allowance(deps, env, info, spender, amount, expiration), ExecuteMsg::TransferFrom { owner, recipient, amount, memo, .. - } => try_transfer_from(deps, &env, info, &mut rng, owner, recipient, amount, memo), + } => execute_transfer_send::try_transfer_from(deps, &env, info, &mut rng, owner, recipient, amount, memo), ExecuteMsg::SendFrom { owner, recipient, @@ -285,7 +297,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S msg, memo, .. - } => try_send_from( + } => execute_transfer_send::try_send_from( deps, env, &info, @@ -298,18 +310,18 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S msg, ), ExecuteMsg::BatchTransferFrom { actions, .. } => { - try_batch_transfer_from(deps, &env, info, &mut rng, actions) + execute_transfer_send::try_batch_transfer_from(deps, &env, info, &mut rng, actions) } ExecuteMsg::BatchSendFrom { actions, .. } => { - try_batch_send_from(deps, env, &info, &mut rng, actions) + execute_transfer_send::try_batch_send_from(deps, env, &info, &mut rng, actions) } ExecuteMsg::BurnFrom { owner, amount, memo, .. - } => try_burn_from(deps, &env, info, owner, amount, memo), - ExecuteMsg::BatchBurnFrom { actions, .. } => try_batch_burn_from(deps, &env, info, actions), + } => execute_mint_burn::try_burn_from(deps, &env, info, owner, amount, memo), + ExecuteMsg::BatchBurnFrom { actions, .. } => execute_mint_burn::try_batch_burn_from(deps, &env, info, actions), // Mint ExecuteMsg::Mint { @@ -317,20 +329,18 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S amount, memo, .. - } => try_mint(deps, env, info, &mut rng, recipient, amount, memo), - ExecuteMsg::BatchMint { actions, .. } => try_batch_mint(deps, env, info, &mut rng, actions), - - // Other - ExecuteMsg::ChangeAdmin { address, .. } => change_admin(deps, info, address), - ExecuteMsg::SetContractStatus { level, .. } => set_contract_status(deps, info, level), - ExecuteMsg::AddMinters { minters, .. } => add_minters(deps, info, minters), - ExecuteMsg::RemoveMinters { minters, .. } => remove_minters(deps, info, minters), - ExecuteMsg::SetMinters { minters, .. } => set_minters(deps, info, minters), - ExecuteMsg::RevokePermit { permit_name, .. } => revoke_permit(deps, info, permit_name), - ExecuteMsg::AddSupportedDenoms { denoms, .. } => add_supported_denoms(deps, info, denoms), - ExecuteMsg::RemoveSupportedDenoms { denoms, .. } => { - remove_supported_denoms(deps, info, denoms) - } + } => execute_mint_burn::try_mint(deps, env, info, &mut rng, recipient, amount, memo), + ExecuteMsg::BatchMint { actions, .. } => execute_mint_burn::try_batch_mint(deps, env, info, &mut rng, actions), + + // SNIP-24 + ExecuteMsg::RevokePermit { permit_name, .. } => execute::revoke_permit(deps, info, permit_name), + + // SNIP-24.1 + ExecuteMsg::RevokeAllPermits { interval, .. } => execute::revoke_all_permits(deps, info, interval), + ExecuteMsg::DeletePermitRevocation { revocation_id, .. } => execute::delete_permit_revocation(deps, info, revocation_id), + + // Admin functions + _ => admin_execute(deps, info, msg) }; let padded_result = pad_handle_result(response, RESPONSE_BLOCK_SIZE); @@ -341,16 +351,46 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S padded_result } +pub fn admin_execute(deps: DepsMut, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + // load contract config from storage + let mut config = CONFIG.load(deps.storage)?; + + // check that message sender is the admin + if config.admin != info.sender { + return Err(StdError::generic_err( + "This is an admin command. Admin commands can only be run from admin address", + )); + } + + match msg { + ExecuteMsg::ChangeAdmin { address, .. } => execute_admin::change_admin(deps, &mut config, address), + ExecuteMsg::SetContractStatus { level, .. } => execute_admin::set_contract_status(deps, level), + ExecuteMsg::AddMinters { minters, .. } => execute_admin::add_minters(deps, &config, minters), + ExecuteMsg::RemoveMinters { minters, .. } => execute_admin::remove_minters(deps, &config, minters), + ExecuteMsg::SetMinters { minters, .. } => execute_admin::set_minters(deps, &config, minters), + ExecuteMsg::AddSupportedDenoms { denoms, .. } => execute_admin::add_supported_denoms(deps, &mut config, denoms), + ExecuteMsg::RemoveSupportedDenoms { denoms, .. } => { + execute_admin::remove_supported_denoms(deps, &mut config, denoms) + }, + + // SNIP-52 + ExecuteMsg::SetNotificationStatus { enabled, .. } => { + execute_admin::set_notification_status(deps, enabled) + }, + _ => panic!("This execute type is not an admin function"), + } +} + #[entry_point] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { pad_query_result( match msg { - QueryMsg::TokenInfo {} => query_token_info(deps.storage), - QueryMsg::TokenConfig {} => query_token_config(deps.storage), - QueryMsg::ContractStatus {} => query_contract_status(deps.storage), - QueryMsg::ExchangeRate {} => query_exchange_rate(deps.storage), - QueryMsg::Minters { .. } => query_minters(deps), - QueryMsg::ListChannels {} => query_list_channels(deps), + QueryMsg::TokenInfo {} => query::query_token_info(deps.storage), + QueryMsg::TokenConfig {} => query::query_token_config(deps.storage), + QueryMsg::ContractStatus {} => query::query_contract_status(deps.storage), + QueryMsg::ExchangeRate {} => query::query_exchange_rate(deps.storage), + QueryMsg::Minters { .. } => query::query_minters(deps), + QueryMsg::ListChannels {} => query::query_list_channels(deps), QueryMsg::WithPermit { permit, query } => permit_queries(deps, env, permit, query), #[cfg(feature = "gas_tracking")] @@ -368,7 +408,7 @@ fn permit_queries(deps: Deps, env: Env, permit: Permit, query: QueryWithPermit) let account = secret_toolkit::permit::validate( deps, - PREFIX_REVOKED_PERMITS, + &env, &permit, token_address.into_string(), None, @@ -384,7 +424,7 @@ fn permit_queries(deps: Deps, env: Env, permit: Permit, query: QueryWithPermit) ))); } - query_balance(deps, account) + query::query_balance(deps, account) } QueryWithPermit::TransferHistory { .. } => { return Err(StdError::generic_err(TRANSFER_HISTORY_UNSUPPORTED_MSG)); @@ -397,7 +437,7 @@ fn permit_queries(deps: Deps, env: Env, permit: Permit, query: QueryWithPermit) ))); } - query_transactions(deps, account, page.unwrap_or(0), page_size) + query::query_transactions(deps, account, page.unwrap_or(0), page_size) } QueryWithPermit::Allowance { owner, spender } => { if !permit.check_permission(&TokenPermissions::Allowance) { @@ -414,7 +454,7 @@ fn permit_queries(deps: Deps, env: Env, permit: Permit, query: QueryWithPermit) ))); } - query_allowance(deps, owner, spender) + query::query_allowance(deps, owner, spender) } QueryWithPermit::AllowancesGiven { owner, @@ -437,7 +477,7 @@ fn permit_queries(deps: Deps, env: Env, permit: Permit, query: QueryWithPermit) permit.params.permissions ))); } - query_allowances_given(deps, account, page.unwrap_or(0), page_size) + query::query_allowances_given(deps, account, page.unwrap_or(0), page_size) } QueryWithPermit::AllowancesReceived { spender, @@ -458,19 +498,28 @@ fn permit_queries(deps: Deps, env: Env, permit: Permit, query: QueryWithPermit) permit.params.permissions ))); } - query_allowances_received(deps, account, page.unwrap_or(0), page_size) + query::query_allowances_received(deps, account, page.unwrap_or(0), page_size) } - QueryWithPermit::ChannelInfo { channels, txhash } => query_channel_info( + QueryWithPermit::ChannelInfo { channels, txhash } => query::query_channel_info( deps, env, channels, txhash, deps.api.addr_canonicalize(account.as_str())?, - ) + ), + QueryWithPermit::ListPermitRevocations { .. } => { + if !permit.check_permission(&TokenPermissions::Owner) { + return Err(StdError::generic_err(format!( + "No permission to query list permit revocations, got permissions {:?}", + permit.params.permissions + ))); + } + query::query_list_permit_revocations(deps, account.as_str()) + }, } } -pub fn viewing_keys_queries(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { +pub fn viewing_keys_queries(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { let (addresses, key) = msg.get_validation_params(deps.api)?; for address in addresses { @@ -478,7 +527,7 @@ pub fn viewing_keys_queries(deps: Deps, env: Env, msg: QueryMsg) -> StdResult query_balance(deps, address), + QueryMsg::Balance { address, .. } => query::query_balance(deps, address), QueryMsg::TransferHistory { .. } => { return Err(StdError::generic_err(TRANSFER_HISTORY_UNSUPPORTED_MSG)); } @@ -487,31 +536,32 @@ pub fn viewing_keys_queries(deps: Deps, env: Env, msg: QueryMsg) -> StdResult query_transactions(deps, address, page.unwrap_or(0), page_size), - QueryMsg::Allowance { owner, spender, .. } => query_allowance(deps, owner, spender), + } => query::query_transactions(deps, address, page.unwrap_or(0), page_size), + QueryMsg::Allowance { owner, spender, .. } => query::query_allowance(deps, owner, spender), QueryMsg::AllowancesGiven { owner, page, page_size, .. - } => query_allowances_given(deps, owner, page.unwrap_or(0), page_size), + } => query::query_allowances_given(deps, owner, page.unwrap_or(0), page_size), QueryMsg::AllowancesReceived { spender, page, page_size, .. - } => query_allowances_received(deps, spender, page.unwrap_or(0), page_size), + } => query::query_allowances_received(deps, spender, page.unwrap_or(0), page_size), QueryMsg::ChannelInfo { channels, txhash, viewer, - } => query_channel_info( + } => query::query_channel_info( deps, env, channels, txhash, deps.api.addr_canonicalize(viewer.address.as_str())?, ), + QueryMsg::ListPermitRevocations{viewer, .. } => query::query_list_permit_revocations(deps, viewer.address.as_str()), _ => panic!("This query type does not require authentication"), }; } @@ -522,2503 +572,45 @@ pub fn viewing_keys_queries(deps: Deps, env: Env, msg: QueryMsg) -> StdResult StdResult { - let constants = CONFIG.load(storage)?; - - if constants.deposit_is_enabled || constants.redeem_is_enabled { - let rate: Uint128; - let denom: String; - // if token has more decimals than SCRT, you get magnitudes of SCRT per token - if constants.decimals >= 6 { - rate = Uint128::new(10u128.pow(constants.decimals as u32 - 6)); - denom = "SCRT".to_string(); - // if token has less decimals, you get magnitudes token for SCRT - } else { - rate = Uint128::new(10u128.pow(6 - constants.decimals as u32)); - denom = constants.symbol; - } - return to_binary(&QueryAnswer::ExchangeRate { rate, denom }); - } - to_binary(&QueryAnswer::ExchangeRate { - rate: Uint128::zero(), - denom: String::new(), - }) -} - -fn query_token_info(storage: &dyn Storage) -> StdResult { - let constants = CONFIG.load(storage)?; - - let total_supply = if constants.total_supply_is_public { - Some(Uint128::new(TOTAL_SUPPLY.load(storage)?)) - } else { - None - }; - - to_binary(&QueryAnswer::TokenInfo { - name: constants.name, - symbol: constants.symbol, - decimals: constants.decimals, - total_supply, - }) -} - -fn query_token_config(storage: &dyn Storage) -> StdResult { - let constants = CONFIG.load(storage)?; - - to_binary(&QueryAnswer::TokenConfig { - public_total_supply: constants.total_supply_is_public, - deposit_enabled: constants.deposit_is_enabled, - redeem_enabled: constants.redeem_is_enabled, - mint_enabled: constants.mint_is_enabled, - burn_enabled: constants.burn_is_enabled, - supported_denoms: constants.supported_denoms, - }) -} - -fn query_contract_status(storage: &dyn Storage) -> StdResult { - let contract_status = CONTRACT_STATUS.load(storage)?; - - to_binary(&QueryAnswer::ContractStatus { - status: contract_status, - }) -} - -pub fn query_transactions( - deps: Deps, - account: String, - page: u32, - page_size: u32, -) -> StdResult { - if page_size == 0 { - return Err(StdError::generic_err("invalid page size")); - } - - // Notice that if query_transactions() was called by a viewing-key call, the address of - // 'account' has already been validated. - // The address of 'account' should not be validated if query_transactions() was called by a - // permit call, for compatibility with non-Secret addresses. - let account = Addr::unchecked(account); - let account_raw = deps.api.addr_canonicalize(account.as_str())?; - - let start = page * page_size; - let mut end = start + page_size; // one more than end index - - // first check if there are any transactions in dwb - let dwb = DWB.load(deps.storage)?; - let dwb_index = dwb.recipient_match(&account_raw); - let mut txs_in_dwb = vec![]; - let txs_in_dwb_count = dwb.entries[dwb_index].list_len()?; - if dwb_index > 0 && txs_in_dwb_count > 0 && start < txs_in_dwb_count as u32 { - // skip if start is after buffer entries - let head_node_index = dwb.entries[dwb_index].head_node()?; - if head_node_index > 0 { - let head_node = TX_NODES - .add_suffix(&head_node_index.to_be_bytes()) - .load(deps.storage)?; - txs_in_dwb = head_node.to_vec(deps.storage, deps.api)?; - } - } - - //let account_slice = account_raw.as_slice(); - let account_stored_entry = stored_entry(deps.storage, &account_raw)?; - let settled_tx_count = stored_tx_count(deps.storage, &account_stored_entry)?; - let total = txs_in_dwb_count as u32 + settled_tx_count as u32; - if end > total { - end = total; - } - - let mut txs: Vec = vec![]; - - let txs_in_dwb_count = txs_in_dwb_count as u32; - if start < txs_in_dwb_count && end < txs_in_dwb_count { - // option 1, start and end are both in dwb - //println!("OPTION 1"); - txs = txs_in_dwb[start as usize..end as usize].to_vec(); // reverse chronological - } else if start < txs_in_dwb_count && end >= txs_in_dwb_count { - // option 2, start is in dwb and end is in settled txs - // in this case, we do not need to search for txs, just begin at last bundle and move backwards - //println!("OPTION 2"); - txs = txs_in_dwb[start as usize..].to_vec(); // reverse chronological - let mut txs_left = (end - start).saturating_sub(txs.len() as u32); - if let Some(entry) = account_stored_entry { - let tx_bundles_idx_len = entry.history_len()?; - if tx_bundles_idx_len > 0 { - let mut bundle_idx = tx_bundles_idx_len - 1; - loop { - let tx_bundle = entry.get_tx_bundle_at(deps.storage, bundle_idx.clone())?; - let head_node = TX_NODES - .add_suffix(&tx_bundle.head_node.to_be_bytes()) - .load(deps.storage)?; - let list_len = tx_bundle.list_len as u32; - if txs_left <= list_len { - txs.extend_from_slice( - &head_node.to_vec(deps.storage, deps.api)?[0..txs_left as usize], - ); - break; - } - txs.extend(head_node.to_vec(deps.storage, deps.api)?); - txs_left = txs_left.saturating_sub(list_len); - if bundle_idx > 0 { - bundle_idx -= 1; - } else { - break; - } - } - } - } - } else if start >= txs_in_dwb_count { - // option 3, start is not in dwb - // in this case, search for where the beginning bundle is using binary search - - // bundle tx offsets are chronological, but we need reverse chronological - // so get the settled start index as if order is reversed - //println!("OPTION 3"); - let settled_start = settled_tx_count - .saturating_sub(start - txs_in_dwb_count) - .saturating_sub(1); - - if let Some((bundle_idx, tx_bundle, start_at)) = - find_start_bundle(deps.storage, &account_raw, settled_start)? - { - let mut txs_left = end - start; - - let head_node = TX_NODES - .add_suffix(&tx_bundle.head_node.to_be_bytes()) - .load(deps.storage)?; - let list_len = tx_bundle.list_len as u32; - if start_at + txs_left <= list_len { - // this first bundle has all the txs we need - txs = head_node.to_vec(deps.storage, deps.api)? - [start_at as usize..(start_at + txs_left) as usize] - .to_vec(); - } else { - // get the rest of the txs in this bundle and then go back through history - txs = head_node.to_vec(deps.storage, deps.api)?[start_at as usize..].to_vec(); - txs_left = txs_left.saturating_sub(list_len - start_at); - - if bundle_idx > 0 && txs_left > 0 { - // get the next earlier bundle - let mut bundle_idx = bundle_idx - 1; - if let Some(entry) = account_stored_entry { - loop { - let tx_bundle = - entry.get_tx_bundle_at(deps.storage, bundle_idx.clone())?; - let head_node = TX_NODES - .add_suffix(&tx_bundle.head_node.to_be_bytes()) - .load(deps.storage)?; - let list_len = tx_bundle.list_len as u32; - if txs_left <= list_len { - txs.extend_from_slice( - &head_node.to_vec(deps.storage, deps.api)? - [0..txs_left as usize], - ); - break; - } - txs.extend(head_node.to_vec(deps.storage, deps.api)?); - txs_left = txs_left.saturating_sub(list_len); - if bundle_idx > 0 { - bundle_idx -= 1; - } else { - break; - } - } - } - } - } - } - } - - let result = QueryAnswer::TransactionHistory { - txs, - total: Some(total as u64), - }; - to_binary(&result) -} - -pub fn query_balance(deps: Deps, account: String) -> StdResult { - // Notice that if query_balance() was called by a viewing key call, the address of 'account' - // has already been validated. - // The address of 'account' should not be validated if query_balance() was called by a permit - // call, for compatibility with non-Secret addresses. - let account = Addr::unchecked(account); - let account = deps.api.addr_canonicalize(account.as_str())?; - - let mut amount = stored_balance(deps.storage, &account)?; - let dwb = DWB.load(deps.storage)?; - let dwb_index = dwb.recipient_match(&account); - if dwb_index > 0 { - amount = amount.saturating_add(dwb.entries[dwb_index].amount()? as u128); - } - let amount = Uint128::new(amount); - let response = QueryAnswer::Balance { amount }; - to_binary(&response) -} - -fn query_minters(deps: Deps) -> StdResult { - let minters = MintersStore::load(deps.storage)?; - - let response = QueryAnswer::Minters { minters }; - to_binary(&response) -} - -// ***************** -// SNIP-52 query functions -// ***************** - -/// -/// ListChannels query -/// -/// Public query to list all notification channels. -/// -fn query_list_channels(deps: Deps) -> StdResult { - let channels: Vec = CHANNELS - .iter(deps.storage)? - .map(|channel| channel.unwrap()) - .collect(); - to_binary(&QueryAnswer::ListChannels { channels }) -} - -/// -/// ChannelInfo query -/// -/// Authenticated query allows clients to obtain the seed, -/// and Notification ID of an event for a specific tx_hash, for a specific channel. -/// -fn query_channel_info( - deps: Deps, - env: Env, - channels: Vec, - txhash: Option, - sender_raw: CanonicalAddr, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - let seed = get_seed(&sender_raw, secret)?; - let mut channels_data = vec![]; - for channel in channels { - let answer_id; - if let Some(tx_hash) = &txhash { - answer_id = Some(notification_id(&seed, &channel, tx_hash)?); - } else { - answer_id = None; - } - match channel.as_str() { - ReceivedNotificationData::CHANNEL_ID => { - let channel_info_data = ChannelInfoData { - mode: "txhash".to_string(), - channel, - answer_id, - parameters: None, - data: None, - next_id: None, - counter: None, - cddl: Some(ReceivedNotificationData::CDDL_SCHEMA.to_string()), - }; - channels_data.push(channel_info_data); - } - SpentNotificationData::CHANNEL_ID => { - let channel_info_data = ChannelInfoData { - mode: "txhash".to_string(), - channel, - answer_id, - parameters: None, - data: None, - next_id: None, - counter: None, - cddl: Some(SpentNotificationData::CDDL_SCHEMA.to_string()), - }; - channels_data.push(channel_info_data); - } - AllowanceNotificationData::CHANNEL_ID => { - let channel_info_data = ChannelInfoData { - mode: "txhash".to_string(), - channel, - answer_id, - parameters: None, - data: None, - next_id: None, - counter: None, - cddl: Some(AllowanceNotificationData::CDDL_SCHEMA.to_string()), - }; - channels_data.push(channel_info_data); - } - MULTI_RECEIVED_CHANNEL_ID => { - let channel_info_data = ChannelInfoData { - mode: "bloom".to_string(), - channel, - answer_id, - parameters: Some(BloomParameters { - m: 512, - k: MULTI_RECEIVED_CHANNEL_BLOOM_K, - h: "sha256".to_string(), - }), - data: Some(Descriptor { - r#type: format!("packet[{}]", MULTI_RECEIVED_CHANNEL_BLOOM_N), - version: "1".to_string(), - packet_size: MULTI_RECEIVED_CHANNEL_PACKET_SIZE, - data: StructDescriptor { - r#type: "struct".to_string(), - label: "transfer".to_string(), - members: vec![ - FlatDescriptor { - r#type: "uint128".to_string(), - label: "amount".to_string(), - description: Some( - "The transfer amount in base denomination".to_string(), - ), - }, - FlatDescriptor { - r#type: "bytes8".to_string(), - label: "spender".to_string(), - description: Some( - "The last 8 bytes of the sender's canonical address" - .to_string(), - ), - }, - ], - }, - }), - counter: None, - next_id: None, - cddl: None, - }; - channels_data.push(channel_info_data); - } - MULTI_SPENT_CHANNEL_ID => { - let channel_info_data = ChannelInfoData { - mode: "bloom".to_string(), - channel, - answer_id, - parameters: Some(BloomParameters { - m: 512, - k: MULTI_SPENT_CHANNEL_BLOOM_K, - h: "sha256".to_string(), - }), - data: Some(Descriptor { - r#type: format!("packet[{}]", MULTI_SPENT_CHANNEL_BLOOM_N), - version: "1".to_string(), - packet_size: MULTI_SPENT_CHANNEL_PACKET_SIZE, - data: StructDescriptor { - r#type: "struct".to_string(), - label: "transfer".to_string(), - members: vec![ - FlatDescriptor { - r#type: "uint128".to_string(), - label: "amount".to_string(), - description: Some( - "The transfer amount in base denomination".to_string(), - ), - }, - FlatDescriptor { - r#type: "uint128".to_string(), - label: "balance".to_string(), - description: Some( - "Spender's new balance after the transfer".to_string(), - ), - }, - FlatDescriptor { - r#type: "bytes8".to_string(), - label: "recipient".to_string(), - description: Some( - "The last 8 bytes of the recipient's canonical address" - .to_string(), - ), - }, - ], - }, - }), - counter: None, - next_id: None, - cddl: None, - }; - channels_data.push(channel_info_data); - } - _ => { - return Err(StdError::generic_err(format!( - "`{}` channel is undefined", - channel - ))); - } - } - } - - //Ok(Binary(vec![])) - //let schema = CHANNEL_SCHEMATA.get(deps.storage, &channel); - - to_binary(&QueryAnswer::ChannelInfo { - as_of_block: Uint64::from(env.block.height), - channels: channels_data, - seed, - }) -} - -// ***************** -// End SNIP-52 query functions -// ***************** - -// execute functions - -fn change_admin(deps: DepsMut, info: MessageInfo, address: String) -> StdResult { - let address = deps.api.addr_validate(address.as_str())?; - - let mut constants = CONFIG.load(deps.storage)?; - check_if_admin(&constants.admin, &info.sender)?; - - constants.admin = address; - CONFIG.save(deps.storage, &constants)?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::ChangeAdmin { status: Success })?)) -} - -fn add_supported_denoms( - deps: DepsMut, - info: MessageInfo, - denoms: Vec, -) -> StdResult { - let mut config = CONFIG.load(deps.storage)?; - - check_if_admin(&config.admin, &info.sender)?; - if !config.can_modify_denoms { - return Err(StdError::generic_err( - "Cannot modify denoms for this contract", - )); - } - - for denom in denoms.iter() { - if !config.supported_denoms.contains(denom) { - config.supported_denoms.push(denom.clone()); - } - } - - CONFIG.save(deps.storage, &config)?; - - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::AddSupportedDenoms { - status: Success, - })?), - ) -} - -fn remove_supported_denoms( - deps: DepsMut, - info: MessageInfo, - denoms: Vec, -) -> StdResult { - let mut config = CONFIG.load(deps.storage)?; - - check_if_admin(&config.admin, &info.sender)?; - if !config.can_modify_denoms { - return Err(StdError::generic_err( - "Cannot modify denoms for this contract", - )); - } - - for denom in denoms.iter() { - config.supported_denoms.retain(|x| x != denom); - } - - CONFIG.save(deps.storage, &config)?; - - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::RemoveSupportedDenoms { - status: Success, - })?), - ) -} - -#[allow(clippy::too_many_arguments)] -fn try_mint_impl( - deps: &mut DepsMut, - rng: &mut ContractPrng, - minter: Addr, - recipient: Addr, - amount: Uint128, - denom: String, - memo: Option, - block: &cosmwasm_std::BlockInfo, - #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, -) -> StdResult<()> { - let raw_amount = amount.u128(); - let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; - let raw_minter = deps.api.addr_canonicalize(minter.as_str())?; - - perform_mint( - deps.storage, - rng, - &raw_minter, - &raw_recipient, - raw_amount, - denom, - memo, - block, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn try_mint( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - rng: &mut ContractPrng, - recipient: String, - amount: Uint128, - memo: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let recipient = deps.api.addr_validate(recipient.as_str())?; - - let constants = CONFIG.load(deps.storage)?; - - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - let minters = MintersStore::load(deps.storage)?; - if !minters.contains(&info.sender) { - return Err(StdError::generic_err( - "Minting is allowed to minter accounts only", - )); - } - - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - let minted_amount = safe_add(&mut total_supply, amount.u128()); - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - // Note that even when minted_amount is equal to 0 we still want to perform the operations for logic consistency - try_mint_impl( - &mut deps, - rng, - info.sender, - recipient.clone(), - Uint128::new(minted_amount), - constants.symbol, - memo, - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - - let received_notification = Notification::new( - recipient, - ReceivedNotificationData { - amount: minted_amount, - sender: None, - }, - ) - .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - let resp = Response::new() - .set_data(to_binary(&ExecuteAnswer::Mint { status: Success })?) - .add_attribute_plaintext( - received_notification.id_plaintext(), - received_notification.data_plaintext(), - ); - - #[cfg(feature = "gas_tracking")] - return Ok(resp.add_gas_tracker(tracker)); - - #[cfg(not(feature = "gas_tracking"))] - Ok(resp) -} - -fn try_batch_mint( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - rng: &mut ContractPrng, - actions: Vec, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let constants = CONFIG.load(deps.storage)?; - - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - let minters = MintersStore::load(deps.storage)?; - if !minters.contains(&info.sender) { - return Err(StdError::generic_err( - "Minting is allowed to minter accounts only", - )); - } - - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - - let mut notifications = vec![]; - // Quick loop to check that the total of amounts is valid - for action in actions { - let actual_amount = safe_add(&mut total_supply, action.amount.u128()); - - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - try_mint_impl( - &mut deps, - rng, - info.sender.clone(), - recipient.clone(), - Uint128::new(actual_amount), - constants.symbol.clone(), - action.memo, - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - notifications.push(Notification::new ( - recipient, - ReceivedNotificationData { - amount: actual_amount, - sender: None, - }, - )); - } - - let tx_hash = env - .transaction - .clone() - .ok_or(StdError::generic_err("no tx hash found"))? - .hash; - let received_data = multi_received_data( - deps.api, - notifications, - &tx_hash, - env.block.random.unwrap(), - secret, - )?; - - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - Ok(Response::new() - .set_data(to_binary(&ExecuteAnswer::BatchMint { status: Success })?) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), - Binary::from(received_data).to_base64(), - ) - ) -} - -pub fn try_set_key(deps: DepsMut, info: MessageInfo, key: String) -> StdResult { - ViewingKey::set(deps.storage, info.sender.as_str(), key.as_str()); - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::SetViewingKey { - status: Success, - })?), - ) -} - -pub fn try_create_key( - deps: DepsMut, - env: Env, - info: MessageInfo, - entropy: Option, - rng: &mut ContractPrng, -) -> StdResult { - let entropy = [entropy.unwrap_or_default().as_bytes(), &rng.rand_bytes()].concat(); - - let key = ViewingKey::create( - deps.storage, - &info, - &env, - info.sender.as_str(), - &entropy, - ); - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::CreateViewingKey { key })?)) -} - -fn set_contract_status( - deps: DepsMut, - info: MessageInfo, - status_level: ContractStatusLevel, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - check_if_admin(&constants.admin, &info.sender)?; - - CONTRACT_STATUS.save(deps.storage, &status_level)?; +// pub fn migrate( +// _deps: DepsMut, +// _env: Env, +// _msg: MigrateMsg, +// ) -> StdResult { +// Ok(MigrateResponse::default()) +// Ok(MigrateResponse::default()) +// } - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::SetContractStatus { - status: Success, - })?), - ) -} +// helper functions -pub fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult { - // Notice that if query_allowance() was called by a viewing-key call, the addresses of 'owner' - // and 'spender' have already been validated. - // The addresses of 'owner' and 'spender' should not be validated if query_allowance() was - // called by a permit call, for compatibility with non-Secret addresses. - let owner = Addr::unchecked(owner); - let spender = Addr::unchecked(spender); - - let allowance = AllowancesStore::load(deps.storage, &owner, &spender); - - let response = QueryAnswer::Allowance { - owner, - spender, - allowance: Uint128::new(allowance.amount), - expiration: allowance.expiration, - }; - to_binary(&response) +fn is_valid_name(name: &str) -> bool { + let len = name.len(); + (3..=30).contains(&len) } -pub fn query_allowances_given( - deps: Deps, - owner: String, - page: u32, - page_size: u32, -) -> StdResult { - // Notice that if query_all_allowances_given() was called by a viewing-key call, - // the address of 'owner' has already been validated. - // The addresses of 'owner' should not be validated if query_all_allowances_given() was - // called by a permit call, for compatibility with non-Secret addresses. - let owner = Addr::unchecked(owner); - - let all_allowances = - AllowancesStore::all_allowances(deps.storage, &owner, page, page_size).unwrap_or(vec![]); - - let allowances_result = all_allowances - .into_iter() - .map(|(spender, allowance)| AllowanceGivenResult { - spender, - allowance: Uint128::from(allowance.amount), - expiration: allowance.expiration, - }) - .collect(); - - let response = QueryAnswer::AllowancesGiven { - owner: owner.clone(), - allowances: allowances_result, - count: AllowancesStore::num_allowances(deps.storage, &owner), - }; - to_binary(&response) -} +fn is_valid_symbol(symbol: &str) -> bool { + let len = symbol.len(); + let len_is_valid = (3..=20).contains(&len); -pub fn query_allowances_received( - deps: Deps, - spender: String, - page: u32, - page_size: u32, -) -> StdResult { - // Notice that if query_all_allowances_received() was called by a viewing-key call, - // the address of 'spender' has already been validated. - // The addresses of 'spender' should not be validated if query_all_allowances_received() was - // called by a permit call, for compatibility with non-Secret addresses. - let spender = Addr::unchecked(spender); - - let all_allowed = - AllowancesStore::all_allowed(deps.storage, &spender, page, page_size).unwrap_or(vec![]); - - let allowances = all_allowed - .into_iter() - .map(|(owner, allowance)| AllowanceReceivedResult { - owner, - allowance: Uint128::from(allowance.amount), - expiration: allowance.expiration, - }) - .collect(); - - let response = QueryAnswer::AllowancesReceived { - spender: spender.clone(), - allowances, - count: AllowancesStore::num_allowed(deps.storage, &spender), - }; - to_binary(&response) + len_is_valid && symbol.bytes().all(|byte| byte.is_ascii_alphabetic()) } -fn try_deposit( - deps: DepsMut, - env: Env, - info: MessageInfo, - rng: &mut ContractPrng, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - - let mut amount = Uint128::zero(); - - for coin in &info.funds { - if constants.supported_denoms.contains(&coin.denom) { - amount += coin.amount - } else { - return Err(StdError::generic_err(format!( - "Tried to deposit an unsupported coin {}", - coin.denom - ))); - } - } - - if amount.is_zero() { - return Err(StdError::generic_err("No funds were sent to be deposited")); - } - - let mut raw_amount = amount.u128(); - - if !constants.deposit_is_enabled { - return Err(StdError::generic_err( - "Deposit functionality is not enabled.", - )); - } - - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - raw_amount = safe_add(&mut total_supply, raw_amount); - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - perform_deposit( - deps.storage, - rng, - &sender_address, - raw_amount, - "uscrt".to_string(), - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - - let resp = Response::new().set_data(to_binary(&ExecuteAnswer::Deposit { status: Success })?); - - #[cfg(feature = "gas_tracking")] - return Ok(resp.add_gas_tracker(tracker)); - - #[cfg(not(feature = "gas_tracking"))] - Ok(resp) -} - -fn try_redeem( - deps: DepsMut, - env: Env, - info: MessageInfo, - amount: Uint128, - denom: Option, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.redeem_is_enabled { - return Err(StdError::generic_err( - "Redeem functionality is not enabled for this token.", - )); - } - - // if denom is none and there is only 1 supported denom then we don't need to check anything - let withdraw_denom = if denom.is_none() && constants.supported_denoms.len() == 1 { - constants.supported_denoms.first().unwrap().clone() - // if denom is specified make sure it's on the list before trying to withdraw with it - } else if denom.is_some() && constants.supported_denoms.contains(denom.as_ref().unwrap()) { - denom.unwrap() - // error handling - } else if denom.is_none() { - return Err(StdError::generic_err( - "Tried to redeem without specifying denom, but multiple coins are supported", - )); - } else { - return Err(StdError::generic_err( - "Tried to redeem for an unsupported coin", - )); - }; - - let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; - let amount_raw = amount.u128(); - - let tx_id = store_redeem_action(deps.storage, amount.u128(), constants.symbol, &env.block)?; - - // load delayed write buffer - let mut dwb = DWB.load(deps.storage)?; - - #[cfg(feature = "gas_tracking")] - let mut tracker = GasTracker::new(deps.api); - - // settle the signer's account in buffer - dwb.settle_sender_or_owner_account( - deps.storage, - &sender_address, - tx_id, - amount_raw, - "redeem", - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - - DWB.save(deps.storage, &dwb)?; - - let total_supply = TOTAL_SUPPLY.load(deps.storage)?; - if let Some(total_supply) = total_supply.checked_sub(amount_raw) { - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - } else { - return Err(StdError::generic_err( - "You are trying to redeem more tokens than what is available in the total supply", - )); - } - - let token_reserve = deps - .querier - .query_balance(&env.contract.address, &withdraw_denom)? - .amount; - if amount > token_reserve { - return Err(StdError::generic_err(format!( - "You are trying to redeem for more {withdraw_denom} than the contract has in its reserve", - ))); - } - - let withdrawal_coins: Vec = vec![Coin { - denom: withdraw_denom, - amount, - }]; - - let message = CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.clone().into_string(), - amount: withdrawal_coins, - }); - let data = to_binary(&ExecuteAnswer::Redeem { status: Success })?; - let res = Response::new().add_message(message).set_data(data); - Ok(res) -} - -#[allow(clippy::too_many_arguments)] -fn try_transfer_impl( - deps: &mut DepsMut, - rng: &mut ContractPrng, - sender: &Addr, - recipient: &Addr, - amount: Uint128, - denom: String, - memo: Option, - block: &cosmwasm_std::BlockInfo, - #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, -) -> StdResult<(Notification, Notification)> { - let raw_sender = deps.api.addr_canonicalize(sender.as_str())?; - let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; - - let sender_balance = perform_transfer( - deps.storage, - rng, - &raw_sender, - &raw_recipient, - &raw_sender, - amount.u128(), - denom, - memo, - block, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - let received_notification = Notification::new( - recipient.clone(), - ReceivedNotificationData { - amount: amount.u128(), - sender: Some(sender.clone()), - } - ); - - let spent_notification = Notification::new ( - sender.clone(), - SpentNotificationData { - amount: amount.u128(), - actions: 1, - recipient: Some(recipient.clone()), - balance: sender_balance, - } - ); - - Ok((received_notification, spent_notification)) -} - -#[allow(clippy::too_many_arguments)] -fn try_transfer( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - rng: &mut ContractPrng, - recipient: String, - amount: Uint128, - memo: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let recipient: Addr = deps.api.addr_validate(recipient.as_str())?; - - let symbol = CONFIG.load(deps.storage)?.symbol; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - let (received_notification, spent_notification) = try_transfer_impl( - &mut deps, - rng, - &info.sender, - &recipient, - amount, - symbol, - memo, - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - - #[cfg(feature = "gas_tracking")] - let mut group1 = tracker.group("try_transfer.rest"); - - let received_notification = received_notification.to_txhash_notification( - deps.api, - &env, - secret, - Some(NOTIFICATION_BLOCK_SIZE), - )?; - - let spent_notification = spent_notification.to_txhash_notification( - deps.api, - &env, - secret, - Some(NOTIFICATION_BLOCK_SIZE) - )?; - - let resp = Response::new() - .set_data(to_binary(&ExecuteAnswer::Transfer { status: Success })?) - .add_attribute_plaintext( - received_notification.id_plaintext(), - received_notification.data_plaintext(), - ) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ); - - #[cfg(feature = "gas_tracking")] - group1.log("rest"); - - #[cfg(feature = "gas_tracking")] - return Ok(resp.add_gas_tracker(tracker)); - - #[cfg(not(feature = "gas_tracking"))] - Ok(resp) -} - -fn try_batch_transfer( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - rng: &mut ContractPrng, - actions: Vec, -) -> StdResult { - let num_actions = actions.len(); - if num_actions == 0 { - return Ok(Response::new() - .set_data(to_binary(&ExecuteAnswer::BatchTransfer { status: Success })?) - ); - } - - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let symbol = CONFIG.load(deps.storage)?.symbol; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - let mut notifications = vec![]; - for action in actions { - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - let (received_notification, spent_notification) = try_transfer_impl( - &mut deps, - rng, - &info.sender, - &recipient, - action.amount, - symbol.clone(), - action.memo, - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - notifications.push((received_notification, spent_notification)); - } - - let tx_hash = env - .transaction - .clone() - .ok_or(StdError::generic_err("no tx hash found"))? - .hash; - let (received_notifications, spent_notifications): ( - Vec>, - Vec>, - ) = notifications.into_iter().unzip(); - let received_data = multi_received_data( - deps.api, - received_notifications, - &tx_hash, - env.block.random.clone().unwrap(), - secret, - )?; - - let total_amount_spent = spent_notifications - .iter() - .fold(0u128, |acc, notification| acc.saturating_add(notification.data.amount)); - - let spent_notification = Notification::new ( - info.sender, - SpentNotificationData { - amount: total_amount_spent, - actions: num_actions as u32, - recipient: spent_notifications[0].data.recipient.clone(), - balance: spent_notifications.last().unwrap().data.balance, - } - ) - .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - let resp = Response::new() - .set_data(to_binary(&ExecuteAnswer::BatchTransfer { status: Success })?) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), - Binary::from(received_data).to_base64(), - ) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ); - - #[cfg(feature = "gas_tracking")] - return Ok(resp.add_gas_tracker(tracker)); - - #[cfg(not(feature = "gas_tracking"))] - Ok(resp) -} - -#[allow(clippy::too_many_arguments)] -fn try_add_receiver_api_callback( - storage: &dyn Storage, - messages: &mut Vec, - recipient: Addr, - recipient_code_hash: Option, - msg: Option, - sender: Addr, - from: Addr, - amount: Uint128, - memo: Option, -) -> StdResult<()> { - if let Some(receiver_hash) = recipient_code_hash { - let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); - let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; - - messages.push(callback_msg); - return Ok(()); - } - - let receiver_hash = ReceiverHashStore::may_load(storage, &recipient)?; - if let Some(receiver_hash) = receiver_hash { - let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); - let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; - - messages.push(callback_msg); - } - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn try_send_impl( - deps: &mut DepsMut, - rng: &mut ContractPrng, - messages: &mut Vec, - sender: Addr, - recipient: Addr, - recipient_code_hash: Option, - amount: Uint128, - denom: String, - memo: Option, - msg: Option, - block: &cosmwasm_std::BlockInfo, - #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, -) -> StdResult<(Notification, Notification)> { - let (received_notification, spent_notification) = try_transfer_impl( - deps, - rng, - &sender, - &recipient, - amount, - denom, - memo.clone(), - block, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - - try_add_receiver_api_callback( - deps.storage, - messages, - recipient, - recipient_code_hash, - msg, - sender.clone(), - sender, - amount, - memo, - )?; - - Ok((received_notification, spent_notification)) -} - -#[allow(clippy::too_many_arguments)] -fn try_send( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - rng: &mut ContractPrng, - recipient: String, - recipient_code_hash: Option, - amount: Uint128, - memo: Option, - msg: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let recipient = deps.api.addr_validate(recipient.as_str())?; - - let mut messages = vec![]; - let symbol = CONFIG.load(deps.storage)?.symbol; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - let (received_notification, spent_notification) = try_send_impl( - &mut deps, - rng, - &mut messages, - info.sender, - recipient, - recipient_code_hash, - amount, - symbol, - memo, - msg, - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - - let received_notification = received_notification.to_txhash_notification( - deps.api, - &env, - secret, - Some(NOTIFICATION_BLOCK_SIZE) - )?; - let spent_notification = - spent_notification.to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - let resp = Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::Send { status: Success })?) - .add_attribute_plaintext( - received_notification.id_plaintext(), - received_notification.data_plaintext(), - ) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ); - - #[cfg(feature = "gas_tracking")] - return Ok(resp.add_gas_tracker(tracker)); - - #[cfg(not(feature = "gas_tracking"))] - Ok(resp) -} - -fn try_batch_send( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - rng: &mut ContractPrng, - actions: Vec, -) -> StdResult { - let num_actions = actions.len(); - if num_actions == 0 { - return Ok(Response::new() - .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?) - ); - } - - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let mut messages = vec![]; - - let mut notifications = vec![]; - let num_actions: usize = actions.len(); - - let symbol = CONFIG.load(deps.storage)?.symbol; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - for action in actions { - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - let (received_notification, spent_notification) = try_send_impl( - &mut deps, - rng, - &mut messages, - info.sender.clone(), - recipient, - action.recipient_code_hash, - action.amount, - symbol.clone(), - action.memo, - action.msg, - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - notifications.push((received_notification, spent_notification)); - } - - let tx_hash = env - .transaction - .clone() - .ok_or(StdError::generic_err("no tx hash found"))? - .hash; - - let (received_notifications, spent_notifications): ( - Vec>, - Vec>, - ) = notifications.into_iter().unzip(); - let received_data = multi_received_data( - deps.api, - received_notifications, - &tx_hash, - env.block.random.clone().unwrap(), - secret, - )?; - - let total_amount_spent = spent_notifications - .iter() - .fold(0u128, |acc, notification| acc + notification.data.amount); - - let spent_notification = Notification::new ( - info.sender, - SpentNotificationData { - amount: total_amount_spent, - actions: num_actions as u32, - recipient: spent_notifications[0].data.recipient.clone(), - balance: spent_notifications.last().unwrap().data.balance, - } - ) - .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), - Binary::from(received_data).to_base64(), - ) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ) - ) -} - -fn try_register_receive( - deps: DepsMut, - info: MessageInfo, - code_hash: String, -) -> StdResult { - ReceiverHashStore::save(deps.storage, &info.sender, code_hash)?; - - let data = to_binary(&ExecuteAnswer::RegisterReceive { status: Success })?; - Ok(Response::new() - .add_attribute("register_status", "success") - .set_data(data)) -} - -fn insufficient_allowance(allowance: u128, required: u128) -> StdError { - StdError::generic_err(format!( - "insufficient allowance: allowance={allowance}, required={required}", - )) -} - -fn use_allowance( - storage: &mut dyn Storage, - env: &Env, - owner: &Addr, - spender: &Addr, - amount: u128, -) -> StdResult<()> { - let mut allowance = AllowancesStore::load(storage, owner, spender); - - if allowance.is_expired_at(&env.block) { - return Err(insufficient_allowance(0, amount)); - } - if let Some(new_allowance) = allowance.amount.checked_sub(amount) { - allowance.amount = new_allowance; - } else { - return Err(insufficient_allowance(allowance.amount, amount)); - } - - AllowancesStore::save(storage, owner, spender, &allowance)?; - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn try_transfer_from_impl( - deps: &mut DepsMut, - rng: &mut ContractPrng, - env: &Env, - spender: &Addr, - owner: &Addr, - recipient: &Addr, - amount: Uint128, - denom: String, - memo: Option, -) -> StdResult<(Notification, Notification)> { - let raw_amount = amount.u128(); - let raw_spender = deps.api.addr_canonicalize(spender.as_str())?; - let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; - let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; - - use_allowance(deps.storage, env, owner, spender, raw_amount)?; - - #[cfg(feature = "gas_tracking")] - let mut tracker: GasTracker = GasTracker::new(deps.api); - - let owner_balance = perform_transfer( - deps.storage, - rng, - &raw_owner, - &raw_recipient, - &raw_spender, - raw_amount, - denom, - memo, - &env.block, - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - - let received_notification = Notification::new( - recipient.clone(), - ReceivedNotificationData { - amount: amount.u128(), - sender: Some(owner.clone()), - } - ); - - let spent_notification = Notification::new ( - owner.clone(), - SpentNotificationData { - amount: amount.u128(), - actions: 1, - recipient: Some(recipient.clone()), - balance: owner_balance, - } - ); - - Ok((received_notification, spent_notification)) -} - -#[allow(clippy::too_many_arguments)] -fn try_transfer_from( - mut deps: DepsMut, - env: &Env, - info: MessageInfo, - rng: &mut ContractPrng, - owner: String, - recipient: String, - amount: Uint128, - memo: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let owner = deps.api.addr_validate(owner.as_str())?; - let recipient = deps.api.addr_validate(recipient.as_str())?; - let symbol = CONFIG.load(deps.storage)?.symbol; - let (received_notification, spent_notification) = try_transfer_from_impl( - &mut deps, - rng, - env, - &info.sender, - &owner, - &recipient, - amount, - symbol, - memo, - )?; - let received_notification = received_notification.to_txhash_notification( - deps.api, - &env, - secret, - Some(NOTIFICATION_BLOCK_SIZE), - )?; - - let spent_notification = spent_notification.to_txhash_notification( - deps.api, - &env, - secret, - Some(NOTIFICATION_BLOCK_SIZE) - )?; - - Ok( - Response::new() - .set_data(to_binary(&ExecuteAnswer::TransferFrom { status: Success })?) - .add_attribute_plaintext( - received_notification.id_plaintext(), - received_notification.data_plaintext(), - ) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ) - ) -} - -fn try_batch_transfer_from( - mut deps: DepsMut, - env: &Env, - info: MessageInfo, - rng: &mut ContractPrng, - actions: Vec, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let mut notifications = vec![]; - - let symbol = CONFIG.load(deps.storage)?.symbol; - for action in actions { - let owner = deps.api.addr_validate(action.owner.as_str())?; - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - let (received_notification, spent_notification) = try_transfer_from_impl( - &mut deps, - rng, - env, - &info.sender, - &owner, - &recipient, - action.amount, - symbol.clone(), - action.memo, - )?; - notifications.push((received_notification, spent_notification)); - } - - let tx_hash = env - .transaction - .clone() - .ok_or(StdError::generic_err("no tx hash found"))? - .hash; - - let (received_notifications, spent_notifications): ( - Vec>, - Vec>, - ) = notifications.into_iter().unzip(); - let received_data = multi_received_data( - deps.api, - received_notifications, - &tx_hash, - env.block.random.clone().unwrap(), - secret, - )?; - let spent_data = multi_spent_data( - deps.api, - spent_notifications, - &tx_hash, - env.block.random.clone().unwrap(), - secret, - )?; - - Ok( - Response::new() - .set_data(to_binary(&ExecuteAnswer::BatchTransferFrom {status: Success})?) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), - Binary::from(received_data).to_base64(), - ) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_SPENT_CHANNEL_ID), - Binary::from(spent_data).to_base64(), - ) - ) -} - -#[allow(clippy::too_many_arguments)] -fn try_send_from_impl( - deps: &mut DepsMut, - env: Env, - info: &MessageInfo, - rng: &mut ContractPrng, - messages: &mut Vec, - owner: Addr, - recipient: Addr, - recipient_code_hash: Option, - amount: Uint128, - memo: Option, - msg: Option, -) -> StdResult<(Notification, Notification)> { - let spender = info.sender.clone(); - let symbol = CONFIG.load(deps.storage)?.symbol; - let (received_notification, spent_notification) = try_transfer_from_impl( - deps, - rng, - &env, - &spender, - &owner, - &recipient, - amount, - symbol, - memo.clone(), - )?; - - try_add_receiver_api_callback( - deps.storage, - messages, - recipient, - recipient_code_hash, - msg, - info.sender.clone(), - owner, - amount, - memo, - )?; - - Ok((received_notification, spent_notification)) -} - -#[allow(clippy::too_many_arguments)] -fn try_send_from( - mut deps: DepsMut, - env: Env, - info: &MessageInfo, - rng: &mut ContractPrng, - owner: String, - recipient: String, - recipient_code_hash: Option, - amount: Uint128, - memo: Option, - msg: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let owner = deps.api.addr_validate(owner.as_str())?; - let recipient = deps.api.addr_validate(recipient.as_str())?; - let mut messages = vec![]; - let (received_notification, spent_notification) = try_send_from_impl( - &mut deps, - env.clone(), - info, - rng, - &mut messages, - owner, - recipient, - recipient_code_hash, - amount, - memo, - msg, - )?; - - let received_notification = received_notification.to_txhash_notification( - deps.api, - &env, - secret, - Some(NOTIFICATION_BLOCK_SIZE), - )?; - let spent_notification = - spent_notification.to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::SendFrom { status: Success })?) - .add_attribute_plaintext( - received_notification.id_plaintext(), - received_notification.data_plaintext(), - ) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ) - ) -} - -fn try_batch_send_from( - mut deps: DepsMut, - env: Env, - info: &MessageInfo, - rng: &mut ContractPrng, - actions: Vec, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let mut messages = vec![]; - let mut notifications = vec![]; - - for action in actions { - let owner = deps.api.addr_validate(action.owner.as_str())?; - let recipient = deps.api.addr_validate(action.recipient.as_str())?; - let (received_notification, spent_notification) = try_send_from_impl( - &mut deps, - env.clone(), - info, - rng, - &mut messages, - owner, - recipient, - action.recipient_code_hash, - action.amount, - action.memo, - action.msg, - )?; - notifications.push((received_notification, spent_notification)); - } - - let tx_hash = env - .transaction - .clone() - .ok_or(StdError::generic_err("no tx hash found"))? - .hash; - - let (received_notifications, spent_notifications): ( - Vec>, - Vec>, - ) = notifications.into_iter().unzip(); - let received_data = multi_received_data( - deps.api, - received_notifications, - &tx_hash, - env.block.random.clone().unwrap(), - secret, - )?; - let spent_data = multi_spent_data( - deps.api, - spent_notifications, - &tx_hash, - env.block.random.clone().unwrap(), - secret, - )?; - - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&ExecuteAnswer::BatchSendFrom { - status: Success, - })?) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_RECEIVED_CHANNEL_ID), - Binary::from(received_data).to_base64(), - ) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_SPENT_CHANNEL_ID), - Binary::from(spent_data).to_base64(), - ) - ) -} - -#[allow(clippy::too_many_arguments)] -fn try_burn_from( - deps: DepsMut, - env: &Env, - info: MessageInfo, - owner: String, - amount: Uint128, - memo: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let owner = deps.api.addr_validate(owner.as_str())?; - let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; - let constants = CONFIG.load(deps.storage)?; - if !constants.burn_is_enabled { - return Err(StdError::generic_err( - "Burn functionality is not enabled for this token.", - )); - } - - let raw_amount = amount.u128(); - use_allowance(deps.storage, env, &owner, &info.sender, raw_amount)?; - let raw_burner = deps.api.addr_canonicalize(info.sender.as_str())?; - - let tx_id = store_burn_action( - deps.storage, - raw_owner.clone(), - raw_burner.clone(), - raw_amount, - constants.symbol, - memo, - &env.block, - )?; - - // load delayed write buffer - let mut dwb = DWB.load(deps.storage)?; - - #[cfg(feature = "gas_tracking")] - let mut tracker = GasTracker::new(deps.api); - - // settle the owner's account in buffer - let owner_balance = dwb.settle_sender_or_owner_account( - deps.storage, - &raw_owner, - tx_id, - raw_amount, - "burn", - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - if raw_burner != raw_owner { - // also settle sender's account - dwb.settle_sender_or_owner_account( - deps.storage, - &raw_burner, - tx_id, - 0, - "burn", - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - } - - DWB.save(deps.storage, &dwb)?; - - // remove from supply - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - - if let Some(new_total_supply) = total_supply.checked_sub(raw_amount) { - total_supply = new_total_supply; - } else { - return Err(StdError::generic_err( - "You're trying to burn more than is available in the total supply", - )); - } - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - let spent_notification = Notification::new ( - owner, - SpentNotificationData { - amount: raw_amount, - actions: 1, - recipient: None, - balance: owner_balance, - } - ) - .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - Ok( - Response::new() - .set_data(to_binary(&ExecuteAnswer::BurnFrom { status: Success })?) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ) - ) -} - -fn try_batch_burn_from( - deps: DepsMut, - env: &Env, - info: MessageInfo, - actions: Vec, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let constants = CONFIG.load(deps.storage)?; - if !constants.burn_is_enabled { - return Err(StdError::generic_err( - "Burn functionality is not enabled for this token.", - )); - } - - let raw_spender = deps.api.addr_canonicalize(info.sender.as_str())?; - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - let mut spent_notifications = vec![]; - - for action in actions { - let owner = deps.api.addr_validate(action.owner.as_str())?; - let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; - let amount = action.amount.u128(); - use_allowance(deps.storage, env, &owner, &info.sender, amount)?; - - let tx_id = store_burn_action( - deps.storage, - raw_owner.clone(), - raw_spender.clone(), - amount, - constants.symbol.clone(), - action.memo.clone(), - &env.block, - )?; - - // load delayed write buffer - let mut dwb = DWB.load(deps.storage)?; - - #[cfg(feature = "gas_tracking")] - let mut tracker = GasTracker::new(deps.api); - - // settle the owner's account in buffer - let owner_balance = dwb.settle_sender_or_owner_account( - deps.storage, - &raw_owner, - tx_id, - amount, - "burn", - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - if raw_spender != raw_owner { - dwb.settle_sender_or_owner_account( - deps.storage, - &raw_spender, - tx_id, - 0, - "burn", - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - } - - DWB.save(deps.storage, &dwb)?; - - // remove from supply - if let Some(new_total_supply) = total_supply.checked_sub(amount) { - total_supply = new_total_supply; - } else { - return Err(StdError::generic_err(format!( - "You're trying to burn more than is available in the total supply: {action:?}", - ))); - } - - spent_notifications.push(Notification::new ( - info.sender.clone(), - SpentNotificationData { - amount, - actions: 1, - recipient: None, - balance: owner_balance, - } - )); - } - - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - let tx_hash = env - .transaction - .clone() - .ok_or(StdError::generic_err("no tx hash found"))? - .hash; - let spent_data = multi_spent_data( - deps.api, - spent_notifications, - &tx_hash, - env.block.random.clone().unwrap(), - secret, - )?; - - Ok( - Response::new() - .set_data(to_binary(&ExecuteAnswer::BatchBurnFrom {status: Success,})?) - .add_attribute_plaintext( - format!("snip52:#{}", MULTI_SPENT_CHANNEL_ID), - Binary::from(spent_data).to_base64(), - ) - ) -} - -fn try_increase_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Uint128, - expiration: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let spender = deps.api.addr_validate(spender.as_str())?; - let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); - - // If the previous allowance has expired, reset the allowance. - // Without this users can take advantage of an expired allowance given to - // them long ago. - if allowance.is_expired_at(&env.block) { - allowance.amount = amount.u128(); - allowance.expiration = None; - } else { - allowance.amount = allowance.amount.saturating_add(amount.u128()); - } - - if expiration.is_some() { - allowance.expiration = expiration; - } - let new_amount = allowance.amount; - AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; - - let notification = Notification::new ( - spender.clone(), - AllowanceNotificationData { - amount: new_amount, - allower: info.sender.clone(), - expiration, - } - ) - .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - Ok( - Response::new() - .set_data(to_binary(&ExecuteAnswer::IncreaseAllowance { - owner: info.sender, - spender, - allowance: Uint128::from(new_amount), - })?) - .add_attribute_plaintext( - notification.id_plaintext(), - notification.data_plaintext() - ) - ) -} - -fn try_decrease_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Uint128, - expiration: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let spender = deps.api.addr_validate(spender.as_str())?; - let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); - - // If the previous allowance has expired, reset the allowance. - // Without this users can take advantage of an expired allowance given to - // them long ago. - if allowance.is_expired_at(&env.block) { - allowance.amount = 0; - allowance.expiration = None; - } else { - allowance.amount = allowance.amount.saturating_sub(amount.u128()); - } - - if expiration.is_some() { - allowance.expiration = expiration; - } - let new_amount = allowance.amount; - AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; - - let notification = Notification::new ( - spender.clone(), - AllowanceNotificationData { - amount: new_amount, - allower: info.sender.clone(), - expiration, - } - ) - .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - Ok( - Response::new() - .set_data(to_binary(&ExecuteAnswer::DecreaseAllowance { - owner: info.sender, - spender, - allowance: Uint128::from(new_amount), - })?) - .add_attribute_plaintext( - notification.id_plaintext(), - notification.data_plaintext() - ) - ) -} - -fn add_minters( - deps: DepsMut, - info: MessageInfo, - minters_to_add: Vec, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - check_if_admin(&constants.admin, &info.sender)?; - - let minters_to_add: Vec = minters_to_add - .iter() - .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) - .collect(); - MintersStore::add_minters(deps.storage, minters_to_add)?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::AddMinters { status: Success })?)) -} - -fn remove_minters( - deps: DepsMut, - info: MessageInfo, - minters_to_remove: Vec, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - check_if_admin(&constants.admin, &info.sender)?; - - let minters_to_remove: StdResult> = minters_to_remove - .iter() - .map(|minter| deps.api.addr_validate(minter.as_str())) - .collect(); - MintersStore::remove_minters(deps.storage, minters_to_remove?)?; - - Ok( - Response::new().set_data(to_binary(&ExecuteAnswer::RemoveMinters { - status: Success, - })?), - ) -} - -fn set_minters( - deps: DepsMut, - info: MessageInfo, - minters_to_set: Vec, -) -> StdResult { - let constants = CONFIG.load(deps.storage)?; - if !constants.mint_is_enabled { - return Err(StdError::generic_err( - "Mint functionality is not enabled for this token.", - )); - } - - check_if_admin(&constants.admin, &info.sender)?; - - let minters_to_set: Vec = minters_to_set - .iter() - .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) - .collect(); - MintersStore::save(deps.storage, minters_to_set)?; - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::SetMinters { status: Success })?)) -} - -/// Burn tokens -/// -/// Remove `amount` tokens from the system irreversibly, from signer account -/// -/// @param amount the amount of money to burn -fn try_burn( - deps: DepsMut, - env: Env, - info: MessageInfo, - amount: Uint128, - memo: Option, -) -> StdResult { - let secret = INTERNAL_SECRET.load(deps.storage)?; - let secret = secret.as_slice(); - - let constants = CONFIG.load(deps.storage)?; - if !constants.burn_is_enabled { - return Err(StdError::generic_err( - "Burn functionality is not enabled for this token.", - )); - } - - let raw_amount = amount.u128(); - let raw_burn_address = deps.api.addr_canonicalize(info.sender.as_str())?; - - let tx_id = store_burn_action( - deps.storage, - raw_burn_address.clone(), - raw_burn_address.clone(), - raw_amount, - constants.symbol, - memo, - &env.block, - )?; - - // load delayed write buffer - let mut dwb = DWB.load(deps.storage)?; - - #[cfg(feature = "gas_tracking")] - let mut tracker = GasTracker::new(deps.api); - - // settle the signer's account in buffer - let owner_balance = dwb.settle_sender_or_owner_account( - deps.storage, - &raw_burn_address, - tx_id, - raw_amount, - "burn", - #[cfg(feature = "gas_tracking")] - &mut tracker, - )?; - - DWB.save(deps.storage, &dwb)?; - - let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; - if let Some(new_total_supply) = total_supply.checked_sub(raw_amount) { - total_supply = new_total_supply; - } else { - return Err(StdError::generic_err( - "You're trying to burn more than is available in the total supply", - )); - } - TOTAL_SUPPLY.save(deps.storage, &total_supply)?; - - let spent_notification = Notification::new ( - info.sender, - SpentNotificationData { - amount: raw_amount, - actions: 1, - recipient: None, - balance: owner_balance, - } - ) - .to_txhash_notification(deps.api, &env, secret, Some(NOTIFICATION_BLOCK_SIZE))?; - - Ok( - Response::new() - .set_data(to_binary(&ExecuteAnswer::Burn { status: Success })?) - .add_attribute_plaintext( - spent_notification.id_plaintext(), - spent_notification.data_plaintext(), - ) - ) -} - -fn perform_transfer( - store: &mut dyn Storage, - rng: &mut ContractPrng, - from: &CanonicalAddr, - to: &CanonicalAddr, - sender: &CanonicalAddr, - amount: u128, - denom: String, - memo: Option, - block: &BlockInfo, - #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, -) -> StdResult { - #[cfg(feature = "gas_tracking")] - let mut group1 = tracker.group("perform_transfer.1"); - - // first store the tx information in the global append list of txs and get the new tx id - let tx_id = store_transfer_action(store, from, sender, to, amount, denom, memo, block)?; - - #[cfg(feature = "gas_tracking")] - group1.log("@store_transfer_action"); - - // load delayed write buffer - let mut dwb = DWB.load(store)?; - - #[cfg(feature = "gas_tracking")] - group1.log("DWB.load"); - - let transfer_str = "transfer"; - - // settle the owner's account - let owner_balance = dwb.settle_sender_or_owner_account( - store, - from, - tx_id, - amount, - transfer_str, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - - // if this is a *_from action, settle the sender's account, too - if sender != from { - dwb.settle_sender_or_owner_account( - store, - sender, - tx_id, - 0, - transfer_str, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - } - - // add the tx info for the recipient to the buffer - dwb.add_recipient( - store, - rng, - to, - tx_id, - amount, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - - #[cfg(feature = "gas_tracking")] - let mut group2 = tracker.group("perform_transfer.2"); - - DWB.save(store, &dwb)?; - - #[cfg(feature = "gas_tracking")] - group2.log("DWB.save"); - - Ok(owner_balance) -} - -fn perform_mint( - store: &mut dyn Storage, - rng: &mut ContractPrng, - minter: &CanonicalAddr, - to: &CanonicalAddr, - amount: u128, - denom: String, - memo: Option, - block: &BlockInfo, - #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, -) -> StdResult<()> { - // first store the tx information in the global append list of txs and get the new tx id - let tx_id = store_mint_action(store, minter, to, amount, denom, memo, block)?; - - // load delayed write buffer - let mut dwb = DWB.load(store)?; - - // if minter is not recipient, settle them - if minter != to { - dwb.settle_sender_or_owner_account( - store, - minter, - tx_id, - 0, - "mint", - #[cfg(feature = "gas_tracking")] - tracker, - )?; - } - - // add the tx info for the recipient to the buffer - dwb.add_recipient( - store, - rng, - to, - tx_id, - amount, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - - DWB.save(store, &dwb)?; - - Ok(()) -} - -fn perform_deposit( - store: &mut dyn Storage, - rng: &mut ContractPrng, - to: &CanonicalAddr, - amount: u128, - denom: String, - block: &BlockInfo, - #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, -) -> StdResult<()> { - // first store the tx information in the global append list of txs and get the new tx id - let tx_id = store_deposit_action(store, amount, denom, block)?; - - // load delayed write buffer - let mut dwb = DWB.load(store)?; - - // add the tx info for the recipient to the buffer - dwb.add_recipient( - store, - rng, - to, - tx_id, - amount, - #[cfg(feature = "gas_tracking")] - tracker, - )?; - - DWB.save(store, &dwb)?; - - Ok(()) -} - -fn revoke_permit(deps: DepsMut, info: MessageInfo, permit_name: String) -> StdResult { - RevokedPermits::revoke_permit( - deps.storage, - PREFIX_REVOKED_PERMITS, - info.sender.as_str(), - &permit_name, - ); - - Ok(Response::new().set_data(to_binary(&ExecuteAnswer::RevokePermit { status: Success })?)) -} - -fn check_if_admin(config_admin: &Addr, account: &Addr) -> StdResult<()> { - if config_admin != account { - return Err(StdError::generic_err( - "This is an admin command. Admin commands can only be run from admin address", - )); - } - - Ok(()) -} - -fn is_valid_name(name: &str) -> bool { - let len = name.len(); - (3..=30).contains(&len) -} - -fn is_valid_symbol(symbol: &str) -> bool { - let len = symbol.len(); - let len_is_valid = (3..=20).contains(&len); - - len_is_valid && symbol.bytes().all(|byte| byte.is_ascii_alphabetic()) -} - -// pub fn migrate( -// _deps: DepsMut, -// _env: Env, -// _msg: MigrateMsg, -// ) -> StdResult { -// Ok(MigrateResponse::default()) -// Ok(MigrateResponse::default()) -// } - #[cfg(test)] mod tests { use std::any::Any; use cosmwasm_std::{ - from_binary, testing::*, Api, BlockInfo, ContractInfo, MessageInfo, OwnedDeps, - QueryResponse, ReplyOn, SubMsg, Timestamp, TransactionInfo, WasmMsg, + from_binary, testing::*, Addr, Api, BlockInfo, Coin, ContractInfo, CosmosMsg, MessageInfo, OwnedDeps, QueryResponse, ReplyOn, SubMsg, Timestamp, TransactionInfo, Uint128, WasmMsg }; use secret_toolkit::permit::{PermitParams, PermitSignature, PubKey}; - use crate::dwb::TX_NODES_COUNT; - use crate::msg::{InitConfig, InitialBalance, ResponseStatus}; - use crate::state::TX_COUNT; - use crate::transaction_history::TxAction; + use crate::batch; + use crate::btbe::stored_balance; + use crate::dwb::{TX_NODES, TX_NODES_COUNT}; + use crate::msg::{ExecuteAnswer, InitConfig, InitialBalance, ResponseStatus, ResponseStatus::Success}; + use crate::receiver::Snip20ReceiveMsg; + use crate::state::{AllowancesStore, ReceiverHashStore, TX_COUNT}; + use crate::transaction_history::{Tx, TxAction}; use super::*; @@ -4263,6 +1855,8 @@ mod tests { permit_name: permit_name.to_string(), chain_id: chain_id.to_string(), permissions: vec![permit_type], + created: None, + expires: None, }, signature: PermitSignature { pub_key: PubKey { diff --git a/src/dwb.rs b/src/dwb.rs index 8b4c7ad..2a7dd25 100644 --- a/src/dwb.rs +++ b/src/dwb.rs @@ -6,7 +6,7 @@ use secret_toolkit_crypto::ContractPrng; use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; -use crate::btbe::{merge_dwb_entry, stored_balance}; +use crate::btbe::{settle_dwb_entry, stored_balance}; use crate::state::{safe_add, safe_add_u64}; use crate::transaction_history::{Tx, TRANSACTIONS}; #[cfg(feature = "gas_tracking")] @@ -87,6 +87,7 @@ impl DelayedWriteBuffer { tx_id: u64, amount_spent: u128, op_name: &str, + is_from_self: bool, #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, ) -> StdResult { #[cfg(feature = "gas_tracking")] @@ -98,6 +99,7 @@ impl DelayedWriteBuffer { #[cfg(feature = "gas_tracking")] group1.log("release_dwb_recipient"); + // check that the owner has sufficient funds to perform the transfer let checked_balance = balance.checked_sub(amount_spent); if checked_balance.is_none() { return Err(StdError::generic_err(format!( @@ -105,24 +107,29 @@ impl DelayedWriteBuffer { ))); }; + // record the event in the dwb entry dwb_entry.add_tx_node(store, tx_id)?; + // *_from action where sender is the owner, repeat the event in history + if is_from_self { + dwb_entry.add_tx_node(store, tx_id)?; + } + #[cfg(feature = "gas_tracking")] group1.log("add_tx_node"); - let mut entry = dwb_entry.clone(); - entry.set_recipient(address)?; + dwb_entry.set_recipient(address)?; #[cfg(feature = "gas_tracking")] group1.logf(format!( "@entry=address:{}, amount:{}", - entry.recipient()?, - entry.amount()? + dwb_entry.recipient()?, + dwb_entry.amount()? )); - merge_dwb_entry( + settle_dwb_entry( store, - &entry, + &mut dwb_entry, Some(amount_spent), #[cfg(feature = "gas_tracking")] tracker, @@ -266,10 +273,10 @@ impl DelayedWriteBuffer { group1.logf(format!("@write_index: {}", write_index)); // settle the entry - let dwb_entry = self.entries[actual_settle_index]; - merge_dwb_entry( + let mut dwb_entry = self.entries[actual_settle_index]; + settle_dwb_entry( store, - &dwb_entry, + &mut dwb_entry, None, #[cfg(feature = "gas_tracking")] tracker, @@ -516,10 +523,15 @@ fn constant_time_is_not_zero(value: i32) -> u32 { } #[inline] -fn constant_time_if_else(condition: u32, then: usize, els: usize) -> usize { +pub fn constant_time_if_else(condition: u32, then: usize, els: usize) -> usize { (then * condition as usize) | (els * (1 - condition as usize)) } +#[inline] +pub fn constant_time_if_else_u32(condition: u32, then: u32, els: u32) -> u32 { + (then * condition) | (els * (1 - condition)) +} + #[cfg(feature = "gas_tracking")] pub fn log_dwb(storage: &dyn Storage) -> StdResult { let dwb = DWB.load(storage)?; diff --git a/src/execute.rs b/src/execute.rs new file mode 100644 index 0000000..09b391f --- /dev/null +++ b/src/execute.rs @@ -0,0 +1,242 @@ +use cosmwasm_std::{to_binary, Addr, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128}; +use secret_toolkit::notification::Notification; +use secret_toolkit::permit::{AllRevokedInterval, RevokedPermits, RevokedPermitsStore}; +use secret_toolkit::viewing_key::{ViewingKey, ViewingKeyStore}; +use secret_toolkit_crypto::ContractPrng; + +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::notifications::AllowanceNotification; +use crate::state::{AllowancesStore, ReceiverHashStore, INTERNAL_SECRET_SENSITIVE, NOTIFICATIONS_ENABLED}; + +// viewing key functions + +pub fn try_set_key(deps: DepsMut, info: MessageInfo, key: String) -> StdResult { + ViewingKey::set(deps.storage, info.sender.as_str(), key.as_str()); + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetViewingKey { + status: Success, + })?), + ) +} + +pub fn try_create_key( + deps: DepsMut, + env: Env, + info: MessageInfo, + entropy: Option, + rng: &mut ContractPrng, +) -> StdResult { + let entropy = [entropy.unwrap_or_default().as_bytes(), &rng.rand_bytes()].concat(); + + let key = ViewingKey::create( + deps.storage, + &info, + &env, + info.sender.as_str(), + &entropy, + ); + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::CreateViewingKey { key })?)) +} + +// register receive function + +pub fn try_register_receive( + deps: DepsMut, + info: MessageInfo, + code_hash: String, +) -> StdResult { + ReceiverHashStore::save(deps.storage, &info.sender, code_hash)?; + + let data = to_binary(&ExecuteAnswer::RegisterReceive { status: Success })?; + Ok(Response::new() + .add_attribute("register_status", "success") + .set_data(data)) +} + +// allowance functions + +fn insufficient_allowance(allowance: u128, required: u128) -> StdError { + StdError::generic_err(format!( + "insufficient allowance: allowance={allowance}, required={required}", + )) +} + +pub fn use_allowance( + storage: &mut dyn Storage, + env: &Env, + owner: &Addr, + spender: &Addr, + amount: u128, +) -> StdResult<()> { + let mut allowance = AllowancesStore::load(storage, owner, spender); + + if allowance.is_expired_at(&env.block) || allowance.amount == 0 { + return Err(insufficient_allowance(0, amount)); + } + if let Some(new_allowance) = allowance.amount.checked_sub(amount) { + allowance.amount = new_allowance; + } else { + return Err(insufficient_allowance(allowance.amount, amount)); + } + + AllowancesStore::save(storage, owner, spender, &allowance)?; + + Ok(()) +} + +pub fn try_increase_allowance( + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expiration: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let spender = deps.api.addr_validate(spender.as_str())?; + let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); + + // If the previous allowance has expired, reset the allowance. + // Without this users can take advantage of an expired allowance given to + // them long ago. + if allowance.is_expired_at(&env.block) { + allowance.amount = amount.u128(); + allowance.expiration = None; + } else { + allowance.amount = allowance.amount.saturating_add(amount.u128()); + } + + if expiration.is_some() { + allowance.expiration = expiration; + } + let new_amount = allowance.amount; + AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::IncreaseAllowance { + owner: info.sender.clone(), + spender: spender.clone(), + allowance: Uint128::from(new_amount), + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let notification = Notification::new ( + spender, + AllowanceNotification { + amount: new_amount, + allower: info.sender, + expiration, + } + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + notification.id_plaintext(), + notification.data_plaintext() + ); + } + + Ok(resp) +} + +pub fn try_decrease_allowance( + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expiration: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let spender = deps.api.addr_validate(spender.as_str())?; + let mut allowance = AllowancesStore::load(deps.storage, &info.sender, &spender); + + // If the previous allowance has expired, reset the allowance. + // Without this users can take advantage of an expired allowance given to + // them long ago. + if allowance.is_expired_at(&env.block) { + allowance.amount = 0; + allowance.expiration = None; + } else { + allowance.amount = allowance.amount.saturating_sub(amount.u128()); + } + + if expiration.is_some() { + allowance.expiration = expiration; + } + let new_amount = allowance.amount; + AllowancesStore::save(deps.storage, &info.sender, &spender, &allowance)?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::DecreaseAllowance { + owner: info.sender.clone(), + spender: spender.clone(), + allowance: Uint128::from(new_amount), + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let notification = Notification::new ( + spender, + AllowanceNotification { + amount: new_amount, + allower: info.sender, + expiration, + } + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + notification.id_plaintext(), + notification.data_plaintext() + ); + } + + Ok(resp) +} + +// SNIP 24, 24.1 permit functions + +pub fn revoke_permit(deps: DepsMut, info: MessageInfo, permit_name: String) -> StdResult { + RevokedPermits::revoke_permit( + deps.storage, + info.sender.as_str(), + &permit_name, + ); + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::RevokePermit { status: Success })?)) +} + +pub fn revoke_all_permits(deps: DepsMut, info: MessageInfo, interval: AllRevokedInterval) -> StdResult { + let revocation_id = RevokedPermits::revoke_all_permits( + deps.storage, + info.sender.as_str(), + &interval, + )?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::RevokeAllPermits { + status: Success, + revocation_id: Some(revocation_id.to_string()), + })?)) +} + +pub fn delete_permit_revocation(deps: DepsMut, info: MessageInfo, revocation_id: String) -> StdResult { + RevokedPermits::delete_revocation( + deps.storage, + info.sender.as_str(), + revocation_id.as_str(), + )?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::DeletePermitRevocation { + status: Success, + })?)) +} + + + + + diff --git a/src/execute_admin.rs b/src/execute_admin.rs new file mode 100644 index 0000000..7d7983c --- /dev/null +++ b/src/execute_admin.rs @@ -0,0 +1,160 @@ +use cosmwasm_std::{to_binary, Addr, DepsMut, Response, StdError, StdResult,}; + +use crate::msg::ContractStatusLevel; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::state::{Config, MintersStore, CONFIG, CONTRACT_STATUS, NOTIFICATIONS_ENABLED}; + +// All the functions in this file MUST only be executed after confirming the sender is the admin + +pub fn change_admin(deps: DepsMut, constants: &mut Config, address: String) -> StdResult { + let address = deps.api.addr_validate(address.as_str())?; + + constants.admin = address; + CONFIG.save(deps.storage, &constants)?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::ChangeAdmin { status: Success })?)) +} + +pub fn add_supported_denoms( + deps: DepsMut, + config: &mut Config, + denoms: Vec, +) -> StdResult { + if !config.can_modify_denoms { + return Err(StdError::generic_err( + "Cannot modify denoms for this contract", + )); + } + + for denom in denoms.iter() { + if !config.supported_denoms.contains(denom) { + config.supported_denoms.push(denom.clone()); + } + } + + CONFIG.save(deps.storage, &config)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::AddSupportedDenoms { + status: Success, + })?), + ) +} + +pub fn remove_supported_denoms( + deps: DepsMut, + config: &mut Config, + denoms: Vec, +) -> StdResult { + if !config.can_modify_denoms { + return Err(StdError::generic_err( + "Cannot modify denoms for this contract", + )); + } + + for denom in denoms.iter() { + config.supported_denoms.retain(|x| x != denom); + } + + CONFIG.save(deps.storage, &config)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::RemoveSupportedDenoms { + status: Success, + })?), + ) +} + +pub fn set_contract_status( + deps: DepsMut, + status_level: ContractStatusLevel, +) -> StdResult { + CONTRACT_STATUS.save(deps.storage, &status_level)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetContractStatus { + status: Success, + })?), + ) +} + +pub fn add_minters( + deps: DepsMut, + constants: &Config, + minters_to_add: Vec, +) -> StdResult { + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters_to_add: Vec = minters_to_add + .iter() + .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) + .collect(); + MintersStore::add_minters(deps.storage, minters_to_add)?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::AddMinters { status: Success })?)) +} + +pub fn remove_minters( + deps: DepsMut, + constants: &Config, + minters_to_remove: Vec, +) -> StdResult { + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters_to_remove: StdResult> = minters_to_remove + .iter() + .map(|minter| deps.api.addr_validate(minter.as_str())) + .collect(); + MintersStore::remove_minters(deps.storage, minters_to_remove?)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::RemoveMinters { + status: Success, + })?), + ) +} + +pub fn set_minters( + deps: DepsMut, + constants: &Config, + minters_to_set: Vec, +) -> StdResult { + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters_to_set: Vec = minters_to_set + .iter() + .map(|minter| deps.api.addr_validate(minter.as_str()).unwrap()) + .collect(); + MintersStore::save(deps.storage, minters_to_set)?; + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::SetMinters { status: Success })?)) +} + +// SNIP-52 functions + +pub fn set_notification_status( + deps: DepsMut, + enabled: bool, +) -> StdResult { + NOTIFICATIONS_ENABLED.save(deps.storage, &enabled)?; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetNotificationStatus { + status: Success, + })?), + ) +} + +// end SNIP-52 functions diff --git a/src/execute_deposit_redeem.rs b/src/execute_deposit_redeem.rs new file mode 100644 index 0000000..394121e --- /dev/null +++ b/src/execute_deposit_redeem.rs @@ -0,0 +1,193 @@ +use cosmwasm_std::{to_binary, BankMsg, BlockInfo, CanonicalAddr, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128}; +use secret_toolkit_crypto::ContractPrng; + +use crate::dwb::DWB; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::state::{safe_add, CONFIG, TOTAL_SUPPLY}; +use crate::transaction_history::{store_deposit_action, store_redeem_action,}; + +// deposit functions + +pub fn try_deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, +) -> StdResult { + let constants = CONFIG.load(deps.storage)?; + + let mut amount = Uint128::zero(); + + for coin in &info.funds { + if constants.supported_denoms.contains(&coin.denom) { + amount += coin.amount + } else { + return Err(StdError::generic_err(format!( + "Tried to deposit an unsupported coin {}", + coin.denom + ))); + } + } + + if amount.is_zero() { + return Err(StdError::generic_err("No funds were sent to be deposited")); + } + + let mut raw_amount = amount.u128(); + + if !constants.deposit_is_enabled { + return Err(StdError::generic_err( + "Deposit functionality is not enabled.", + )); + } + + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + raw_amount = safe_add(&mut total_supply, raw_amount); + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + perform_deposit( + deps.storage, + rng, + &sender_address, + raw_amount, + "uscrt".to_string(), + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + let resp = Response::new().set_data(to_binary(&ExecuteAnswer::Deposit { status: Success })?); + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +fn perform_deposit( + store: &mut dyn Storage, + rng: &mut ContractPrng, + to: &CanonicalAddr, + amount: u128, + denom: String, + block: &BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<()> { + // first store the tx information in the global append list of txs and get the new tx id + let tx_id = store_deposit_action(store, amount, denom, block)?; + + // load delayed write buffer + let mut dwb = DWB.load(store)?; + + // add the tx info for the recipient to the buffer + dwb.add_recipient( + store, + rng, + to, + tx_id, + amount, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + DWB.save(store, &dwb)?; + + Ok(()) +} + +// redeem functions + +pub fn try_redeem( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + denom: Option, +) -> StdResult { + let constants = CONFIG.load(deps.storage)?; + if !constants.redeem_is_enabled { + return Err(StdError::generic_err( + "Redeem functionality is not enabled for this token.", + )); + } + + // if denom is none and there is only 1 supported denom then we don't need to check anything + let withdraw_denom = if denom.is_none() && constants.supported_denoms.len() == 1 { + constants.supported_denoms.first().unwrap().clone() + // if denom is specified make sure it's on the list before trying to withdraw with it + } else if denom.is_some() && constants.supported_denoms.contains(denom.as_ref().unwrap()) { + denom.unwrap() + // error handling + } else if denom.is_none() { + return Err(StdError::generic_err( + "Tried to redeem without specifying denom, but multiple coins are supported", + )); + } else { + return Err(StdError::generic_err( + "Tried to redeem for an unsupported coin", + )); + }; + + let sender_address = deps.api.addr_canonicalize(info.sender.as_str())?; + let amount_raw = amount.u128(); + + let tx_id = store_redeem_action(deps.storage, amount.u128(), constants.symbol, &env.block)?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the signer's account in buffer + dwb.settle_sender_or_owner_account( + deps.storage, + &sender_address, + tx_id, + amount_raw, + "redeem", + false, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + DWB.save(deps.storage, &dwb)?; + + let total_supply = TOTAL_SUPPLY.load(deps.storage)?; + if let Some(total_supply) = total_supply.checked_sub(amount_raw) { + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + } else { + return Err(StdError::generic_err( + "You are trying to redeem more tokens than what is available in the total supply", + )); + } + + let token_reserve = deps + .querier + .query_balance(&env.contract.address, &withdraw_denom)? + .amount; + if amount > token_reserve { + return Err(StdError::generic_err(format!( + "You are trying to redeem for more {withdraw_denom} than the contract has in its reserve", + ))); + } + + let withdrawal_coins: Vec = vec![Coin { + denom: withdraw_denom, + amount, + }]; + + let message = CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.clone().into_string(), + amount: withdrawal_coins, + }); + let data = to_binary(&ExecuteAnswer::Redeem { status: Success })?; + let res = Response::new().add_message(message).set_data(data); + Ok(res) +} diff --git a/src/execute_mint_burn.rs b/src/execute_mint_burn.rs new file mode 100644 index 0000000..83f66d4 --- /dev/null +++ b/src/execute_mint_burn.rs @@ -0,0 +1,570 @@ +use cosmwasm_std::{to_binary, Addr, BlockInfo, CanonicalAddr, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128}; +use secret_toolkit::notification::Notification; +use secret_toolkit_crypto::ContractPrng; + +use crate::batch; +use crate::dwb::DWB; +use crate::execute::use_allowance; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::notifications::{render_group_notification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, SpentNotification}; +use crate::state::{safe_add, MintersStore, CONFIG, INTERNAL_SECRET_SENSITIVE, NOTIFICATIONS_ENABLED, TOTAL_SUPPLY}; +use crate::transaction_history::{store_burn_action, store_mint_action}; + +// mint functions + +#[allow(clippy::too_many_arguments)] +pub fn try_mint( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + recipient: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let recipient = deps.api.addr_validate(recipient.as_str())?; + + let constants = CONFIG.load(deps.storage)?; + + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters = MintersStore::load(deps.storage)?; + if !minters.contains(&info.sender) { + return Err(StdError::generic_err( + "Minting is allowed to minter accounts only", + )); + } + + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + let minted_amount = safe_add(&mut total_supply, amount.u128()); + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // Note that even when minted_amount is equal to 0 we still want to perform the operations for logic consistency + try_mint_impl( + &mut deps, + rng, + info.sender, + recipient.clone(), + Uint128::new(minted_amount), + constants.symbol, + memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::Mint { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = Notification::new( + recipient, + RecvdNotification { + amount: minted_amount, + sender: None, + memo_len, + sender_is_owner: true, + }, + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ); + } + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +pub fn try_batch_mint( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let constants = CONFIG.load(deps.storage)?; + + if !constants.mint_is_enabled { + return Err(StdError::generic_err( + "Mint functionality is not enabled for this token.", + )); + } + + let minters = MintersStore::load(deps.storage)?; + if !minters.contains(&info.sender) { + return Err(StdError::generic_err( + "Minting is allowed to minter accounts only", + )); + } + + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + + let mut notifications = vec![]; + // Quick loop to check that the total of amounts is valid + for action in actions { + let actual_amount = safe_add(&mut total_supply, action.amount.u128()); + + let recipient = deps.api.addr_validate(action.recipient.as_str())?; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + notifications.push(Notification::new ( + recipient.clone(), + RecvdNotification { + amount: actual_amount, + sender: None, + memo_len: action.memo.as_ref().map(|s| s.len()).unwrap_or_default(), + sender_is_owner: true, + }, + )); + + try_mint_impl( + &mut deps, + rng, + info.sender.clone(), + recipient, + Uint128::new(actual_amount), + constants.symbol.clone(), + action.memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + } + + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchMint { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + resp = render_group_notification( + deps.api, + MultiRecvdNotification(notifications), + &env.transaction.unwrap().hash, + env.block.random.unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} + +#[allow(clippy::too_many_arguments)] +fn try_mint_impl( + deps: &mut DepsMut, + rng: &mut ContractPrng, + minter: Addr, + recipient: Addr, + amount: Uint128, + denom: String, + memo: Option, + block: &cosmwasm_std::BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<()> { + let raw_amount = amount.u128(); + let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; + let raw_minter = deps.api.addr_canonicalize(minter.as_str())?; + + perform_mint( + deps.storage, + rng, + &raw_minter, + &raw_recipient, + raw_amount, + denom, + memo, + block, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + Ok(()) +} + +pub fn perform_mint( + store: &mut dyn Storage, + rng: &mut ContractPrng, + minter: &CanonicalAddr, + to: &CanonicalAddr, + amount: u128, + denom: String, + memo: Option, + block: &BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<()> { + // first store the tx information in the global append list of txs and get the new tx id + let tx_id = store_mint_action(store, minter, to, amount, denom, memo, block)?; + + // load delayed write buffer + let mut dwb = DWB.load(store)?; + + // sender and owner are different + if minter != to { + // settle the sender's account too + dwb.settle_sender_or_owner_account( + store, + minter, + tx_id, + 0, + "mint", + false, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + } + + // add the tx info for the recipient to the buffer + dwb.add_recipient( + store, + rng, + to, + tx_id, + amount, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + DWB.save(store, &dwb)?; + + Ok(()) +} + +// burn functions + +/// Burn tokens +/// +/// Remove `amount` tokens from the system irreversibly, from signer account +/// +/// @param amount the amount of money to burn +pub fn try_burn( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let constants = CONFIG.load(deps.storage)?; + if !constants.burn_is_enabled { + return Err(StdError::generic_err( + "Burn functionality is not enabled for this token.", + )); + } + + let raw_amount = amount.u128(); + let raw_burn_address = deps.api.addr_canonicalize(info.sender.as_str())?; + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + let tx_id = store_burn_action( + deps.storage, + raw_burn_address.clone(), + raw_burn_address.clone(), + raw_amount, + constants.symbol, + memo, + &env.block, + )?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the signer's account in buffer + let owner_balance = dwb.settle_sender_or_owner_account( + deps.storage, + &raw_burn_address, + tx_id, + raw_amount, + "burn", + false, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + DWB.save(deps.storage, &dwb)?; + + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + if let Some(new_total_supply) = total_supply.checked_sub(raw_amount) { + total_supply = new_total_supply; + } else { + return Err(StdError::generic_err( + "You're trying to burn more than is available in the total supply", + )); + } + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::Burn { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let spent_notification = Notification::new ( + info.sender, + SpentNotification { + amount: raw_amount, + actions: 1, + recipient: None, + balance: owner_balance, + memo_len, + } + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +#[allow(clippy::too_many_arguments)] +pub fn try_burn_from( + deps: DepsMut, + env: &Env, + info: MessageInfo, + owner: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let owner = deps.api.addr_validate(owner.as_str())?; + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; + let constants = CONFIG.load(deps.storage)?; + if !constants.burn_is_enabled { + return Err(StdError::generic_err( + "Burn functionality is not enabled for this token.", + )); + } + + let raw_amount = amount.u128(); + use_allowance(deps.storage, env, &owner, &info.sender, raw_amount)?; + let raw_burner = deps.api.addr_canonicalize(info.sender.as_str())?; + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // store the event + let tx_id = store_burn_action( + deps.storage, + raw_owner.clone(), + raw_burner.clone(), + raw_amount, + constants.symbol, + memo, + &env.block, + )?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the owner's account in buffer + let owner_balance = dwb.settle_sender_or_owner_account( + deps.storage, + &raw_owner, + tx_id, + raw_amount, + "burn", + raw_burner == raw_owner, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + // sender and owner are different + if raw_burner != raw_owner { + // also settle sender's account + dwb.settle_sender_or_owner_account( + deps.storage, + &raw_burner, + tx_id, + 0, + "burn", + false, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + } + + DWB.save(deps.storage, &dwb)?; + + // remove from supply + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + + if let Some(new_total_supply) = total_supply.checked_sub(raw_amount) { + total_supply = new_total_supply; + } else { + return Err(StdError::generic_err( + "You're trying to burn more than is available in the total supply", + )); + } + + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::BurnFrom { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let spent_notification = Notification::new ( + owner, + SpentNotification { + amount: raw_amount, + actions: 1, + recipient: None, + balance: owner_balance, + memo_len, + } + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +pub fn try_batch_burn_from( + deps: DepsMut, + env: &Env, + info: MessageInfo, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let constants = CONFIG.load(deps.storage)?; + if !constants.burn_is_enabled { + return Err(StdError::generic_err( + "Burn functionality is not enabled for this token.", + )); + } + + let raw_spender = deps.api.addr_canonicalize(info.sender.as_str())?; + let mut total_supply = TOTAL_SUPPLY.load(deps.storage)?; + let mut spent_notifications = vec![]; + + for action in actions { + let owner = deps.api.addr_validate(action.owner.as_str())?; + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; + let amount = action.amount.u128(); + use_allowance(deps.storage, env, &owner, &info.sender, amount)?; + + let tx_id = store_burn_action( + deps.storage, + raw_owner.clone(), + raw_spender.clone(), + amount, + constants.symbol.clone(), + action.memo.clone(), + &env.block, + )?; + + // load delayed write buffer + let mut dwb = DWB.load(deps.storage)?; + + #[cfg(feature = "gas_tracking")] + let mut tracker = GasTracker::new(deps.api); + + // settle the owner's account in buffer + let owner_balance = dwb.settle_sender_or_owner_account( + deps.storage, + &raw_owner, + tx_id, + amount, + "burn", + raw_spender == raw_owner, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + // sender and owner are different + if raw_spender != raw_owner { + // also settle the sender's account + dwb.settle_sender_or_owner_account( + deps.storage, + &raw_spender, + tx_id, + 0, + "burn", + false, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + } + + DWB.save(deps.storage, &dwb)?; + + // remove from supply + if let Some(new_total_supply) = total_supply.checked_sub(amount) { + total_supply = new_total_supply; + } else { + return Err(StdError::generic_err(format!( + "You're trying to burn more than is available in the total supply: {action:?}", + ))); + } + + spent_notifications.push(Notification::new ( + info.sender.clone(), + SpentNotification { + amount, + actions: 1, + recipient: None, + balance: owner_balance, + memo_len: action.memo.as_ref().map(|s| s.len()).unwrap_or_default() + } + )); + } + + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchBurnFrom {status: Success,})?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + resp = render_group_notification( + deps.api, + MultiSpentNotification(spent_notifications), + &env.transaction.clone().unwrap().hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} \ No newline at end of file diff --git a/src/execute_transfer_send.rs b/src/execute_transfer_send.rs new file mode 100644 index 0000000..4d6988d --- /dev/null +++ b/src/execute_transfer_send.rs @@ -0,0 +1,977 @@ +use cosmwasm_std::{to_binary, Addr, Binary, BlockInfo, CanonicalAddr, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128}; +use secret_toolkit::notification::Notification; +use secret_toolkit_crypto::ContractPrng; + +use crate::batch; +use crate::dwb::DWB; +use crate::execute::use_allowance; +use crate::msg::{ExecuteAnswer, ResponseStatus::Success}; +use crate::notifications::{render_group_notification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, SpentNotification}; +use crate::receiver::Snip20ReceiveMsg; +use crate::state::{ReceiverHashStore, CONFIG, INTERNAL_SECRET_SENSITIVE, NOTIFICATIONS_ENABLED}; +use crate::strings::SEND_TO_CONTRACT_ERR_MSG; +use crate::transaction_history::store_transfer_action; + +// transfer functions + +#[allow(clippy::too_many_arguments)] +pub fn try_transfer( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + recipient: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let recipient: Addr = deps.api.addr_validate(recipient.as_str())?; + + let symbol = CONFIG.load(deps.storage)?.symbol; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + // perform the transfer + let ( + received_notification, + spent_notification + ) = try_transfer_impl( + &mut deps, + rng, + &info.sender, + &recipient, + amount, + symbol, + memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("try_transfer.rest"); + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::Transfer { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + // render the tokens received notification + let received_notification = received_notification.to_txhash_notification( + deps.api, + &env, + secret, + None, + )?; + + // render the tokens spent notification + let spent_notification = spent_notification.to_txhash_notification( + deps.api, + &env, + secret, + None, + )?; + + resp = resp.add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + #[cfg(feature = "gas_tracking")] + group1.log("rest"); + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +pub fn try_batch_transfer( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let num_actions = actions.len(); + if num_actions == 0 { + return Ok(Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchTransfer { status: Success })?) + ); + } + + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let symbol = CONFIG.load(deps.storage)?.symbol; + + let mut total_memo_len = 0; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + let mut notifications = vec![]; + for action in actions { + let recipient = deps.api.addr_validate(action.recipient.as_str())?; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + total_memo_len += action.memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + let ( + received_notification, + spent_notification + ) = try_transfer_impl( + &mut deps, + rng, + &info.sender, + &recipient, + action.amount, + symbol.clone(), + action.memo, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + notifications.push((received_notification, spent_notification)); + } + + let ( + received_notifications, + spent_notifications + ): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchTransfer { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &env.transaction.clone().unwrap().hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + let total_amount_spent = spent_notifications + .iter() + .fold(0u128, |acc, notification| acc.saturating_add(notification.data.amount)); + + let spent_notification = Notification::new ( + info.sender, + SpentNotification { + amount: total_amount_spent, + actions: num_actions as u32, + recipient: spent_notifications[0].data.recipient.clone(), + balance: spent_notifications.last().unwrap().data.balance, + memo_len: total_memo_len, + } + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +#[allow(clippy::too_many_arguments)] +pub fn try_transfer_from( + mut deps: DepsMut, + env: &Env, + info: MessageInfo, + rng: &mut ContractPrng, + owner: String, + recipient: String, + amount: Uint128, + memo: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let owner = deps.api.addr_validate(owner.as_str())?; + let recipient = deps.api.addr_validate(recipient.as_str())?; + let symbol = CONFIG.load(deps.storage)?.symbol; + let ( + received_notification, + spent_notification + ) = try_transfer_from_impl( + &mut deps, + rng, + env, + &info.sender, + &owner, + &recipient, + amount, + symbol, + memo, + )?; + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::TransferFrom { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = received_notification.to_txhash_notification( + deps.api, + &env, + secret, + None, + )?; + + let spent_notification = spent_notification.to_txhash_notification( + deps.api, + &env, + secret, + None + )?; + + resp = resp.add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +pub fn try_batch_transfer_from( + mut deps: DepsMut, + env: &Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let mut notifications = vec![]; + + let symbol = CONFIG.load(deps.storage)?.symbol; + for action in actions { + let owner = deps.api.addr_validate(action.owner.as_str())?; + let recipient = deps.api.addr_validate(action.recipient.as_str())?; + + let ( + received_notification, + spent_notification + ) = try_transfer_from_impl( + &mut deps, + rng, + env, + &info.sender, + &owner, + &recipient, + action.amount, + symbol.clone(), + action.memo, + )?; + + notifications.push((received_notification, spent_notification)); + } + + let mut resp = Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchTransferFrom {status: Success})?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let (received_notifications, spent_notifications): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + let tx_hash = env.transaction.clone().unwrap().hash; + + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + resp = render_group_notification( + deps.api, + MultiSpentNotification(spent_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} + +// send functions + +#[allow(clippy::too_many_arguments)] +pub fn try_send( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + recipient: String, + recipient_code_hash: Option, + amount: Uint128, + memo: Option, + msg: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let recipient = deps.api.addr_validate(recipient.as_str())?; + + let mut messages = vec![]; + let symbol = CONFIG.load(deps.storage)?.symbol; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + let ( + received_notification, + spent_notification + ) = try_send_impl( + &mut deps, + rng, + &mut messages, + info.sender, + recipient, + recipient_code_hash, + amount, + symbol, + memo, + msg, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + let mut resp = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::Send { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = received_notification.to_txhash_notification(deps.api, &env, secret, None)?; + let spent_notification = spent_notification.to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + #[cfg(feature = "gas_tracking")] + return Ok(resp.add_gas_tracker(tracker)); + + #[cfg(not(feature = "gas_tracking"))] + Ok(resp) +} + +pub fn try_batch_send( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let num_actions = actions.len(); + if num_actions == 0 { + return Ok(Response::new() + .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?) + ); + } + + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let mut messages = vec![]; + + let mut notifications = vec![]; + let num_actions: usize = actions.len(); + + let symbol = CONFIG.load(deps.storage)?.symbol; + + let mut total_memo_len = 0; + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + for action in actions { + let recipient = deps.api.addr_validate(action.recipient.as_str())?; + + // make sure the sender is not accidentally sending tokens to the contract address + if recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + total_memo_len += action.memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + let ( + received_notification, + spent_notification + ) = try_send_impl( + &mut deps, + rng, + &mut messages, + info.sender.clone(), + recipient, + action.recipient_code_hash, + action.amount, + symbol.clone(), + action.memo, + action.msg, + &env.block, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + notifications.push((received_notification, spent_notification)); + } + + let mut resp = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::BatchSend { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let (received_notifications, spent_notifications): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &env.transaction.clone().unwrap().hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + let total_amount_spent = spent_notifications + .iter() + .fold(0u128, |acc, notification| acc + notification.data.amount); + + let spent_notification = Notification::new ( + info.sender, + SpentNotification { + amount: total_amount_spent, + actions: num_actions as u32, + recipient: spent_notifications[0].data.recipient.clone(), + balance: spent_notifications.last().unwrap().data.balance, + memo_len: total_memo_len, + } + ) + .to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ); + } + + Ok(resp) +} + +#[allow(clippy::too_many_arguments)] +pub fn try_send_from( + mut deps: DepsMut, + env: Env, + info: &MessageInfo, + rng: &mut ContractPrng, + owner: String, + recipient: String, + recipient_code_hash: Option, + amount: Uint128, + memo: Option, + msg: Option, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let owner = deps.api.addr_validate(owner.as_str())?; + let recipient = deps.api.addr_validate(recipient.as_str())?; + let mut messages = vec![]; + let ( + received_notification, + spent_notification + ) = try_send_from_impl( + &mut deps, + env.clone(), + info, + rng, + &mut messages, + owner, + recipient, + recipient_code_hash, + amount, + memo, + msg, + )?; + + let mut resp = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::SendFrom { status: Success })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let received_notification = received_notification.to_txhash_notification(deps.api, &env, secret, None,)?; + let spent_notification = spent_notification.to_txhash_notification(deps.api, &env, secret, None)?; + + resp = resp.add_attribute_plaintext( + received_notification.id_plaintext(), + received_notification.data_plaintext(), + ) + .add_attribute_plaintext( + spent_notification.id_plaintext(), + spent_notification.data_plaintext(), + ) + } + + Ok(resp) +} + +pub fn try_batch_send_from( + mut deps: DepsMut, + env: Env, + info: &MessageInfo, + rng: &mut ContractPrng, + actions: Vec, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + + let mut messages = vec![]; + let mut notifications = vec![]; + + for action in actions { + let owner = deps.api.addr_validate(action.owner.as_str())?; + let recipient = deps.api.addr_validate(action.recipient.as_str())?; + let ( + received_notification, + spent_notification + ) = try_send_from_impl( + &mut deps, + env.clone(), + info, + rng, + &mut messages, + owner, + recipient, + action.recipient_code_hash, + action.amount, + action.memo, + action.msg, + )?; + notifications.push((received_notification, spent_notification)); + } + + let mut resp = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::BatchSendFrom { + status: Success, + })?); + + if NOTIFICATIONS_ENABLED.load(deps.storage)? { + let (received_notifications, spent_notifications): ( + Vec>, + Vec>, + ) = notifications.into_iter().unzip(); + + let tx_hash = env.transaction.clone().unwrap().hash; + + resp = render_group_notification( + deps.api, + MultiRecvdNotification(received_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + + resp = render_group_notification( + deps.api, + MultiSpentNotification(spent_notifications), + &tx_hash, + env.block.random.clone().unwrap(), + secret, + resp, + )?; + } + + Ok(resp) +} + +// helper functions + +#[allow(clippy::too_many_arguments)] +fn try_transfer_impl( + deps: &mut DepsMut, + rng: &mut ContractPrng, + owner: &Addr, + recipient: &Addr, + amount: Uint128, + denom: String, + memo: Option, + block: &cosmwasm_std::BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<(Notification, Notification)> { + // canonicalize owner and recipient addresses + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; + let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; + + // memo length + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // create the tokens received notification for recipient + let received_notification = Notification::new( + recipient.clone(), + RecvdNotification { + amount: amount.u128(), + sender: Some(owner.clone()), + memo_len, + sender_is_owner: true, + } + ); + + // perform the transfer from owner to recipient + let owner_balance = perform_transfer( + deps.storage, + rng, + &raw_owner, + &raw_recipient, + &raw_owner, + amount.u128(), + denom, + memo.clone(), + block, + false, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + // create the tokens spent notification for owner + let spent_notification = Notification::new ( + owner.clone(), + SpentNotification { + amount: amount.u128(), + actions: 1, + recipient: Some(recipient.clone()), + balance: owner_balance, + memo_len, + } + ); + + Ok((received_notification, spent_notification)) +} + +#[allow(clippy::too_many_arguments)] +fn try_transfer_from_impl( + deps: &mut DepsMut, + rng: &mut ContractPrng, + env: &Env, + spender: &Addr, + owner: &Addr, + recipient: &Addr, + amount: Uint128, + denom: String, + memo: Option, +) -> StdResult<(Notification, Notification)> { + let raw_amount = amount.u128(); + let raw_spender = deps.api.addr_canonicalize(spender.as_str())?; + let raw_owner = deps.api.addr_canonicalize(owner.as_str())?; + let raw_recipient = deps.api.addr_canonicalize(recipient.as_str())?; + + use_allowance(deps.storage, env, owner, spender, raw_amount)?; + + // make sure the sender is not accidentally sending tokens to the contract address + if *recipient == env.contract.address { + return Err(StdError::generic_err(SEND_TO_CONTRACT_ERR_MSG)); + } + + #[cfg(feature = "gas_tracking")] + let mut tracker: GasTracker = GasTracker::new(deps.api); + + let memo_len = memo.as_ref().map(|s| s.len()).unwrap_or_default(); + + // create tokens received notification for recipient + let received_notification = Notification::new( + recipient.clone(), + RecvdNotification { + amount: amount.u128(), + sender: Some(owner.clone()), + memo_len, + sender_is_owner: spender == owner, + } + ); + + // perform the transfer from owner to recipient + let owner_balance = perform_transfer( + deps.storage, + rng, + &raw_owner, + &raw_recipient, + &raw_spender, + raw_amount, + denom, + memo, + &env.block, + true, + #[cfg(feature = "gas_tracking")] + &mut tracker, + )?; + + // create tokens spent notification for owner + let spent_notification = Notification::new ( + owner.clone(), + SpentNotification { + amount: amount.u128(), + actions: 1, + recipient: Some(recipient.clone()), + balance: owner_balance, + memo_len, + } + ); + + Ok((received_notification, spent_notification)) +} + +#[allow(clippy::too_many_arguments)] +fn try_send_impl( + deps: &mut DepsMut, + rng: &mut ContractPrng, + messages: &mut Vec, + sender: Addr, + recipient: Addr, + recipient_code_hash: Option, + amount: Uint128, + denom: String, + memo: Option, + msg: Option, + block: &cosmwasm_std::BlockInfo, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult<(Notification, Notification)> { + let ( + received_notification, + spent_notification + ) = try_transfer_impl( + deps, + rng, + &sender, + &recipient, + amount, + denom, + memo.clone(), + block, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + try_add_receiver_api_callback( + deps.storage, + messages, + recipient, + recipient_code_hash, + msg, + sender.clone(), + sender, + amount, + memo, + )?; + + Ok((received_notification, spent_notification)) +} + +#[allow(clippy::too_many_arguments)] +fn try_send_from_impl( + deps: &mut DepsMut, + env: Env, + info: &MessageInfo, + rng: &mut ContractPrng, + messages: &mut Vec, + owner: Addr, + recipient: Addr, + recipient_code_hash: Option, + amount: Uint128, + memo: Option, + msg: Option, +) -> StdResult<(Notification, Notification)> { + let spender = info.sender.clone(); + let symbol = CONFIG.load(deps.storage)?.symbol; + let ( + received_notification, + spent_notification + ) = try_transfer_from_impl( + deps, + rng, + &env, + &spender, + &owner, + &recipient, + amount, + symbol, + memo.clone(), + )?; + + try_add_receiver_api_callback( + deps.storage, + messages, + recipient, + recipient_code_hash, + msg, + info.sender.clone(), + owner, + amount, + memo, + )?; + + Ok((received_notification, spent_notification)) +} + +fn perform_transfer( + store: &mut dyn Storage, + rng: &mut ContractPrng, + from: &CanonicalAddr, + to: &CanonicalAddr, + sender: &CanonicalAddr, + amount: u128, + denom: String, + memo: Option, + block: &BlockInfo, + is_from_action: bool, + #[cfg(feature = "gas_tracking")] tracker: &mut GasTracker, +) -> StdResult { + #[cfg(feature = "gas_tracking")] + let mut group1 = tracker.group("perform_transfer.1"); + + // first store the tx information in the global append list of txs and get the new tx id + let tx_id = store_transfer_action(store, from, sender, to, amount, denom, memo, block)?; + + #[cfg(feature = "gas_tracking")] + group1.log("@store_transfer_action"); + + // load delayed write buffer + let mut dwb = DWB.load(store)?; + + #[cfg(feature = "gas_tracking")] + group1.log("DWB.load"); + + let transfer_str = "transfer"; + + // settle the owner's account + let owner_balance = dwb.settle_sender_or_owner_account( + store, + from, + tx_id, + amount, + transfer_str, + is_from_action && sender == from, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + // sender and owner are different + if sender != from { + // settle the sender's account too + dwb.settle_sender_or_owner_account( + store, + sender, + tx_id, + 0, + transfer_str, + false, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + } + + // add the tx info for the recipient to the buffer + dwb.add_recipient( + store, + rng, + to, + tx_id, + amount, + #[cfg(feature = "gas_tracking")] + tracker, + )?; + + #[cfg(feature = "gas_tracking")] + let mut group2 = tracker.group("perform_transfer.2"); + + DWB.save(store, &dwb)?; + + #[cfg(feature = "gas_tracking")] + group2.log("DWB.save"); + + Ok(owner_balance) +} + +#[allow(clippy::too_many_arguments)] +fn try_add_receiver_api_callback( + storage: &dyn Storage, + messages: &mut Vec, + recipient: Addr, + recipient_code_hash: Option, + msg: Option, + sender: Addr, + from: Addr, + amount: Uint128, + memo: Option, +) -> StdResult<()> { + if let Some(receiver_hash) = recipient_code_hash { + let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); + let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; + + messages.push(callback_msg); + return Ok(()); + } + + let receiver_hash = ReceiverHashStore::may_load(storage, &recipient)?; + if let Some(receiver_hash) = receiver_hash { + let receiver_msg = Snip20ReceiveMsg::new(sender, from, amount, memo, msg); + let callback_msg = receiver_msg.into_cosmos_msg(receiver_hash, recipient)?; + + messages.push(callback_msg); + } + Ok(()) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b992851..bbe941b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,15 @@ extern crate static_assertions as sa; mod batch; mod btbe; +mod constants; pub mod contract; mod dwb; +pub mod execute; +pub mod execute_admin; +pub mod execute_deposit_redeem; +pub mod execute_mint_burn; +pub mod execute_transfer_send; +pub mod query; mod gas_tracker; pub mod msg; pub mod receiver; diff --git a/src/msg.rs b/src/msg.rs index 09d0be7..fc6e1f7 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -7,7 +7,7 @@ use crate::{batch, transaction_history::Tx}; use cosmwasm_std::{Addr, Api, Binary, StdError, StdResult, Uint128, Uint64,}; #[cfg(feature = "gas_evaporation")] use cosmwasm_std::Uint64; -use secret_toolkit::{notification::ChannelInfoData, permit::Permit}; +use secret_toolkit::{notification::ChannelInfoData, permit::{AllRevocation, AllRevokedInterval, Permit}}; #[cfg_attr(test, derive(Eq, PartialEq))] #[derive(Serialize, Deserialize, Clone, JsonSchema)] @@ -283,6 +283,12 @@ pub enum ExecuteMsg { #[cfg(feature = "gas_evaporation")] gas_target: Option, }, + /// Enable or disable SNIP-52 notifications + SetNotificationStatus { + enabled: bool, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, // Permit RevokePermit { @@ -291,6 +297,27 @@ pub enum ExecuteMsg { gas_target: Option, padding: Option, }, + + // SNIP 24.1 Blanket Permits + + /// Revokes all permits. Client can supply a datetime for created_after, created_before, both, or neither. + /// * created_before – makes it so any permits using a created value less than this datetime will be rejected + /// * created_after – makes it so any permits using a created value greater than this datetime will be rejected + /// * both created_before and created_after – makes it so any permits using a created value between these two datetimes will be rejected + /// * neither – makes it so ANY permit will be rejected. + /// in this case, the contract MUST return a revocation ID of "REVOKED_ALL". this action is idempotent + RevokeAllPermits { + interval: AllRevokedInterval, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, + + /// Deletes a previously issued permit revocation. + DeletePermitRevocation { + revocation_id: String, + #[cfg(feature = "gas_evaporation")] + gas_target: Option, + }, } #[derive(Serialize, Deserialize, JsonSchema, Debug)] @@ -390,11 +417,25 @@ pub enum ExecuteAnswer { RemoveSupportedDenoms { status: ResponseStatus, }, + SetNotificationStatus { + status: ResponseStatus, + }, // Permit RevokePermit { status: ResponseStatus, }, + + // SNIP 24.1 - Blanket Permits + RevokeAllPermits { + status: ResponseStatus, + revocation_id: Option, + }, + + DeletePermitRevocation { + status: ResponseStatus, + }, + } #[cfg(feature = "gas_evaporation")] @@ -433,12 +474,15 @@ impl Evaporator for ExecuteMsg { | ExecuteMsg::SetContractStatus { gas_target, .. } | ExecuteMsg::AddSupportedDenoms { gas_target, .. } | ExecuteMsg::RemoveSupportedDenoms { gas_target, .. } - | ExecuteMsg::RevokePermit { gas_target, .. } => match gas_target { + | ExecuteMsg::SetNotificationStatus { gas_targe, .. } + | ExecuteMsg::RevokePermit { gas_target, .. } + | ExecuteMsg::RevokeAllPermits { gas_target, .. } + | ExecuteMsg::DeletePermitRevocation { gas_target, .. } => match gas_target { Some(gas_target) => { let gas_used = api.check_gas()?; if gas_used < gas_target.u64() { let evaporate_amount = gas_target.u64() - gas_used; - // api.gas_evaporate(evaporate_amount as u32)?; + api.gas_evaporate(evaporate_amount as u32)?; return Ok(evaporate_amount) } Ok(0) @@ -503,6 +547,15 @@ pub enum QueryMsg { viewer: ViewerInfo, }, + // SNIP 24.1 + ListPermitRevocations { + // `page` and `page_size` do nothing here because max revocations is only 10 but included + // to satisfy the SNIP24.1 spec + page: Option, + page_size: Option, + viewer: ViewerInfo, + }, + WithPermit { permit: Permit, query: QueryWithPermit, @@ -560,6 +613,10 @@ impl QueryMsg { let address = api.addr_validate(viewer.address.as_str())?; Ok((vec![address], viewer.viewing_key.clone())) } + Self::ListPermitRevocations { viewer, .. } => { + let address = api.addr_validate(viewer.address.as_str())?; + Ok((vec![address], viewer.viewing_key.clone())) + } _ => panic!("This query type does not require authentication"), } } @@ -597,6 +654,13 @@ pub enum QueryWithPermit { channels: Vec, txhash: Option, }, + // SNIP 24.1 + ListPermitRevocations { + // `page` and `page_size` do nothing here because max revocations is only 10 but included + // to satisfy the SNIP24.1 spec + page: Option, + page_size: Option, + }, } #[derive(Serialize, Deserialize, JsonSchema, Debug)] @@ -665,6 +729,11 @@ pub enum QueryAnswer { channels: Vec, }, + // SNIP-24.1 + ListPermitRevocations { + revocations: Vec, + }, + #[cfg(feature = "gas_tracking")] Dwb { dwb: String, diff --git a/src/notifications.rs b/src/notifications.rs index 38e76f7..2ceafa5 100644 --- a/src/notifications.rs +++ b/src/notifications.rs @@ -1,402 +1,426 @@ use std::collections::HashMap; -use cosmwasm_std::{Addr, Api, Binary, CanonicalAddr, StdError, StdResult}; +use cosmwasm_std::{Addr, Api, Binary, CanonicalAddr, Response, StdResult}; use primitive_types::{U256, U512}; -use secret_toolkit::notification::{get_seed, notification_id, xor_bytes, Notification, NotificationData}; -use minicbor_ser as cbor; +use secret_toolkit::notification::{get_seed, notification_id, xor_bytes, EncoderExt, CBL_ADDRESS, CBL_ARRAY_SHORT, CBL_BIGNUM_U64, CBL_TIMESTAMP, CBL_U8, Notification, DirectChannel, GroupChannel}; +use minicbor::Encoder; use secret_toolkit_crypto::{hkdf_sha_512, sha_256}; use serde::{Deserialize, Serialize}; + const ZERO_ADDR: [u8; 20] = [0u8; 20]; -// recvd = [ -// amount: biguint, ; transfer amount in base denomination -// sender: bstr, ; byte sequence of sender's canonical address -// balance: biguint ; recipient's new balance after the transfer -// ] +// maximum value that can be stored in 62 bits +const U62_MAX: u128 = (1 << 62) - 1; + +// maximum value that can be stored in 63 bits +const U63_MAX: u128 = (1 << 63) - 1; + #[derive(Serialize, Debug, Deserialize, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] -pub struct ReceivedNotificationData { +pub struct RecvdNotification { pub amount: u128, pub sender: Option, + pub memo_len: usize, + pub sender_is_owner: bool, } -impl NotificationData for ReceivedNotificationData { - const CHANNEL_ID: &'static str = "recvd"; - const CDDL_SCHEMA: &'static str = "recvd=[amount:biguint,sender:bstr]"; - - fn to_cbor(&self, api: &dyn Api) -> StdResult> { - let received_data; +/// ```cddl +/// recvd = [ +/// amount: biguint .size 8, ; transfer amount in base denomination +/// sender: bstr .size 20, ; number of actions the execution performed +/// memo_len: uint .size 1, ; byte sequence of first recipient's canonical address +/// ] +/// ``` +impl DirectChannel for RecvdNotification { + const CHANNEL_ID: &'static str = "recvd"; + const CDDL_SCHEMA: &'static str = "recvd=[amount:biguint .size 8,sender:bstr .size 20,memo_len:uint .size 1]"; + const ELEMENTS: u64 = 3; + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_ADDRESS + CBL_U8; + + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { + // amount:biguint (8-byte uint) + encoder.ext_u64_from_u128(self.amount)?; + + // sender:bstr (20-byte address) if let Some(sender) = &self.sender { let sender_raw = api.addr_canonicalize(sender.as_str())?; - received_data = cbor::to_vec(&(self.amount.to_be_bytes(), sender_raw.as_slice())) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + encoder.ext_address(sender_raw)?; } else { - received_data = cbor::to_vec(&(self.amount.to_be_bytes(), ZERO_ADDR)) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + encoder.ext_bytes(&ZERO_ADDR)?; } - Ok(received_data) + + // memo_len:uint (1-byte uint) + encoder.ext_u8(self.memo_len.clamp(0, u8::MAX.into()) as u8)?; + + Ok(()) } } -// spent = [ -// amount: biguint, ; transfer amount in base denomination -// actions: uint ; number of actions the execution performed -// recipient: bstr, ; byte sequence of first recipient's canonical address -// balance: biguint ; sender's new balance aactions: uint ; number of actions the execution performedfter the transfer -// ] - +/// ```cddl +/// spent = [ +/// amount: biguint .size 8, ; transfer amount in base denomination +/// actions: uint .size 1, ; number of actions the execution performed +/// recipient: bstr .size 20, ; byte sequence of first recipient's canonical address +/// balance: biguint .size 8, ; sender's new balance aactions +/// ] +/// ``` #[derive(Serialize, Debug, Deserialize, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] -pub struct SpentNotificationData { +pub struct SpentNotification { pub amount: u128, pub actions: u32, pub recipient: Option, pub balance: u128, + pub memo_len: usize, } -impl NotificationData for SpentNotificationData { + +impl DirectChannel for SpentNotification { const CHANNEL_ID: &'static str = "spent"; - const CDDL_SCHEMA: &'static str = "spent=[amount:biguint,actions:uint,recipient:bstr,balance:biguint]"; - fn to_cbor(&self, api: &dyn Api) -> StdResult> { - let spent_data; + const CDDL_SCHEMA: &'static str = "spent=[amount:biguint .size 8,actions:uint .size 1,recipient:bstr .size 20,balance:biguint .size 8]"; + const ELEMENTS: u64 = 4; + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_U8 + CBL_ADDRESS + CBL_BIGNUM_U64; + + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { + // amount:biguint (8-byte uint), actions:uint (1-byte uint) + let mut spent_data = encoder + .ext_u64_from_u128(self.amount)? + .ext_u8(self.actions.clamp(0, u8::MAX.into()) as u8)?; + + // recipient:bstr (20-byte address) if let Some(recipient) = &self.recipient { let recipient_raw = api.addr_canonicalize(recipient.as_str())?; - spent_data = cbor::to_vec(&( - self.amount.to_be_bytes(), - self.actions.to_be_bytes(), - recipient_raw.as_slice(), - self.balance.to_be_bytes(), - )) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + spent_data = spent_data.ext_address(recipient_raw)?; } else { - spent_data = cbor::to_vec(&( - self.amount.to_be_bytes(), - self.actions.to_be_bytes(), - ZERO_ADDR, - self.balance.to_be_bytes(), - )) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; + spent_data = spent_data.ext_bytes(&ZERO_ADDR)? } - Ok(spent_data) + + // balance:biguint (8-byte uint) + spent_data.ext_u64_from_u128(self.balance)?; + + Ok(()) } } -//allowance = [ -// amount: biguint, ; allowance amount in base denomination -// allower: bstr, ; byte sequence of allower's canonical address -// expiration: uint, ; epoch seconds of allowance expiration -//] +///```cddl +/// allowance = [ +/// amount: biguint .size 8, ; allowance amount in base denomination +/// allower: bstr .size 20, ; byte sequence of allower's canonical address +/// expiration: uint .size 8, ; epoch seconds of allowance expiration +///] +/// ``` #[derive(Serialize, Debug, Deserialize, Clone)] #[cfg_attr(test, derive(Eq, PartialEq))] -pub struct AllowanceNotificationData { +pub struct AllowanceNotification { pub amount: u128, pub allower: Addr, pub expiration: Option, } -impl NotificationData for AllowanceNotificationData { +impl DirectChannel for AllowanceNotification { const CHANNEL_ID: &'static str = "allowance"; - const CDDL_SCHEMA: &'static str = "allowance=[amount:biguint,allower:bstr,expiration:uint]"; - fn to_cbor(&self, api: &dyn Api) -> StdResult> { + const CDDL_SCHEMA: &'static str = "allowance=[amount:biguint .size 8,allower:bstr .size 20,expiration:uint .size 8]"; + const ELEMENTS: u64 = 3; + const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_ADDRESS + CBL_TIMESTAMP; + + fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { let allower_raw = api.addr_canonicalize(self.allower.as_str())?; - // use CBOR to encode data - let updated_allowance_data = cbor::to_vec(&( - self.amount.to_be_bytes(), - allower_raw.as_slice(), - self.expiration.unwrap_or(0u64), // expiration == 0 means no expiration - )) - .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; - Ok(updated_allowance_data) + // amount:biguint (8-byte uint), allower:bstr (20-byte address), expiration:uint (8-byte timestamp) + encoder + .ext_u64_from_u128(self.amount)? + .ext_bytes(allower_raw.as_slice())? + .ext_timestamp(self.expiration.unwrap_or_default())?; + + Ok(()) } } -// multi recipient push notifications +pub struct MultiRecvdNotification(pub Vec>); -// id for the `multirecvd` channel -pub const MULTI_RECEIVED_CHANNEL_ID: &str = "multirecvd"; -pub const MULTI_RECEIVED_CHANNEL_BLOOM_K: u32 = 15; -pub const MULTI_RECEIVED_CHANNEL_BLOOM_N: u32 = 16; -pub const MULTI_RECEIVED_CHANNEL_PACKET_SIZE: u32 = 24; +impl GroupChannel for MultiRecvdNotification { + const CHANNEL_ID: &'static str = "multirecvd"; -// id for the `multispent` channel -pub const MULTI_SPENT_CHANNEL_ID: &str = "multispent"; -pub const MULTI_SPENT_CHANNEL_BLOOM_K: u32 = 5; -pub const MULTI_SPENT_CHANNEL_BLOOM_N: u32 = 4; -pub const MULTI_SPENT_CHANNEL_PACKET_SIZE: u32 = 40; + // bloom parameters for the `multirecvd` channel: + const BLOOM_N: usize = 16; + const BLOOM_M: u32 = 512; + const BLOOM_K: u32 = 22; -pub fn multi_received_data( - api: &dyn Api, - notifications: Vec>, - tx_hash: &String, - env_random: Binary, - secret: &[u8], -) -> StdResult> { - let mut received_bloom_filter: U512 = U512::from(0); - let mut received_packets: Vec<(Addr, Vec)> = vec![]; - - // keep track of how often addresses might show up in packet data. - // we need to remove any address that might show up more than once. - let mut recipient_counts: HashMap = HashMap::new(); - - for notification in ¬ifications { - recipient_counts.insert( - notification.notification_for.clone(), - recipient_counts - .get(¬ification.notification_for) - .unwrap_or(&0u16) - + 1, - ); - - // we can short circuit this if recipient count > 1, since we will throw out this packet - // anyway, and address has already been added to bloom filter - if *recipient_counts - .get(¬ification.notification_for) - .unwrap() - > 1 - { - continue; - } + // flagsAndAmount:8 + ownerId:8 == 16 bytes + const PACKET_SIZE: usize = 16; - // contribute to received bloom filter - let recipient_addr_raw = api.addr_canonicalize(notification.notification_for.as_str())?; - let seed = get_seed(&recipient_addr_raw, secret)?; - let id = notification_id(&seed, &MULTI_RECEIVED_CHANNEL_ID.to_string(), &tx_hash)?; - let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); - for _ in 0..MULTI_RECEIVED_CHANNEL_BLOOM_K { - let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); - received_bloom_filter = received_bloom_filter | (U512::from(1) << bit_index); - hash_bytes = hash_bytes >> 9; - } + fn notifications(&self) -> &Vec> { + &self.0 + } + fn build_packet(&self, api: &dyn Api, data: &RecvdNotification) -> StdResult> { // make the received packet - let mut received_packet_plaintext: Vec = vec![]; - // amount bytes (u128 == 16 bytes) - received_packet_plaintext.extend_from_slice(¬ification.data.amount.to_be_bytes()); - // sender account last 8 bytes - let sender_bytes: &[u8]; - let sender_raw; - if let Some(sender) = ¬ification.data.sender { - sender_raw = api.addr_canonicalize(sender.as_str())?; - sender_bytes = &sender_raw.as_slice()[sender_raw.0.len() - 8..]; + let mut packet_plaintext = [0u8; Self::PACKET_SIZE]; + + // encode flags and amount into 8 bytes (leftmost 2 bits reserved) + let amount_bytes = &(data.amount.clamp(0, U62_MAX) + | (((data.memo_len != 0) as u128) << 63) + | ((data.sender_is_owner as u128) << 62) + ).to_be_bytes()[8..]; + + // packet flag bits and amount bytes (u64 == 8 bytes) + packet_plaintext[0..8].copy_from_slice(amount_bytes); + + // determine owner address + let owner_addr: CanonicalAddr; + let owner_bytes: &[u8]; + if let Some(owner) = &data.sender { + owner_addr = api.addr_canonicalize(owner.as_str())?; + owner_bytes = &owner_addr.as_slice() } else { - sender_bytes = &ZERO_ADDR[ZERO_ADDR.len() - 8..]; + owner_bytes = &ZERO_ADDR; } - // 24 bytes total - received_packet_plaintext.extend_from_slice(sender_bytes); - let received_packet_id = &id.0.as_slice()[0..8]; - let received_packet_ikm = &id.0.as_slice()[8..32]; - let received_packet_ciphertext = - xor_bytes(received_packet_plaintext.as_slice(), received_packet_ikm); - let received_packet_bytes: Vec = - [received_packet_id.to_vec(), received_packet_ciphertext].concat(); + // packet owner address terminal 8 bytes (8 bytes) + packet_plaintext[8..16].copy_from_slice(&owner_bytes[12..]); - received_packets.push((notification.notification_for.clone(), received_packet_bytes)); + // 16 bytes total + Ok(packet_plaintext.to_vec()) } +} - // filter out any notifications for recipients showing up more than once - let mut received_packets: Vec> = received_packets - .into_iter() - .filter(|(addr, _)| *recipient_counts.get(addr).unwrap_or(&0u16) <= 1) - .map(|(_, packet)| packet) - .collect(); - if received_packets.len() > MULTI_RECEIVED_CHANNEL_BLOOM_N as usize { - // still too many packets - received_packets = received_packets[0..MULTI_RECEIVED_CHANNEL_BLOOM_N as usize].to_vec(); +// maximum supported filter size is currently 512 bits +const_assert!(MultiRecvdNotification::BLOOM_M <= 512); + +// ensure m is a power of 2 +const_assert!(MultiRecvdNotification::BLOOM_M.trailing_zeros() == MultiRecvdNotification::BLOOM_M_LOG2); + +// ensure there are enough bits in the 32-byte source hash to provide entropy for the hashes +const_assert!(MultiRecvdNotification::BLOOM_K * MultiRecvdNotification::BLOOM_M_LOG2 <= 256); + +// this implementation is optimized to not check for packet sizes larger than 24 bytes +const_assert!(MultiRecvdNotification::PACKET_SIZE <= 24); + + +pub struct MultiSpentNotification(pub Vec>); + + +impl GroupChannel for MultiSpentNotification { + const CHANNEL_ID: &str = "multispent"; + + // bloom parameters for the `multispent` channel: + const BLOOM_N: usize = 4; + const BLOOM_M: u32 = 128; + const BLOOM_K: u32 = 22; + + // flagsAndAmount:8 + recipientId:8 + balance:8 == 24 bytes + const PACKET_SIZE: usize = 24; + + fn notifications(&self) -> &Vec> { + &self.0 } - // now add extra packets, if needed, to hide number of packets - let padding_size = - MULTI_RECEIVED_CHANNEL_BLOOM_N.saturating_sub(received_packets.len() as u32) as usize; - if padding_size > 0 { - let padding_addresses = hkdf_sha_512( - &Some(vec![0u8; 64]), - &env_random, - format!("{}:decoys", MULTI_RECEIVED_CHANNEL_ID).as_bytes(), - padding_size * 20, // 20 bytes per random addr - )?; + fn build_packet(&self, api: &dyn Api, data: &SpentNotification) -> StdResult> { + // prep the packet plaintext + let mut packet_plaintext = [0u8; Self::PACKET_SIZE]; - // handle each padding package - for i in 0..padding_size { - let padding_address = &padding_addresses[i * 20..(i + 1) * 20]; - - // contribute padding packet to bloom filter - let seed = get_seed(&CanonicalAddr::from(padding_address), secret)?; - let id = notification_id(&seed, &MULTI_RECEIVED_CHANNEL_ID.to_string(), &tx_hash)?; - let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); - for _ in 0..MULTI_RECEIVED_CHANNEL_BLOOM_K { - let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); - received_bloom_filter = received_bloom_filter | (U512::from(1) << bit_index); - hash_bytes = hash_bytes >> 9; - } - - // padding packet plaintext - let padding_packet_plaintext = [0u8; MULTI_RECEIVED_CHANNEL_PACKET_SIZE as usize]; - let padding_packet_id = &id.0.as_slice()[0..8]; - let padding_packet_ikm = &id.0.as_slice()[8..32]; - let padding_packet_ciphertext = - xor_bytes(padding_packet_plaintext.as_slice(), padding_packet_ikm); - let padding_packet_bytes: Vec = - [padding_packet_id.to_vec(), padding_packet_ciphertext].concat(); - received_packets.push(padding_packet_bytes); + // encode flags and amount into 8 bytes (leftmost 2 bits reserved) + let amount_bytes = &(data.amount.clamp(0, U62_MAX) + | (((data.memo_len != 0) as u128) << 63) + ).to_be_bytes()[8..]; + + // packet flags and amount bytes (u64 == 8 bytes) + packet_plaintext[0..8].copy_from_slice(amount_bytes); + + // determine recipient address + let recipient_addr: CanonicalAddr; + let recipient_bytes: &[u8]; + if let Some(recipient) = &data.recipient { + recipient_addr = api.addr_canonicalize(recipient.as_str())?; + recipient_bytes = recipient_addr.as_slice(); + } else { + recipient_bytes = &ZERO_ADDR; } - } - let mut received_bloom_filter_bytes: Vec = vec![]; - for biguint in received_bloom_filter.0 { - received_bloom_filter_bytes.extend_from_slice(&biguint.to_be_bytes()); - } - for packet in received_packets { - received_bloom_filter_bytes.extend(packet.iter()); + // packet recipient address terminal 8 bytes (8 bytes) + packet_plaintext[8..16].copy_from_slice(&recipient_bytes[12..]); + + // balance bytes (u64 == 8 bytes) + packet_plaintext[16..24].copy_from_slice( + &data.balance + .clamp(0, u64::MAX.into()) + .to_be_bytes()[8..] + ); + + // 24 bytes total + Ok(packet_plaintext.to_vec()) } +} + +// maximum supported filter size is currently 512 bits +const_assert!(MultiSpentNotification::BLOOM_M <= 512); + +// ensure m is a power of 2 +const_assert!(MultiSpentNotification::BLOOM_M.trailing_zeros() == MultiSpentNotification::BLOOM_M_LOG2); + +// ensure there are enough bits in the 32-byte source hash to provide entropy for the hashes +const_assert!(MultiSpentNotification::BLOOM_K * MultiSpentNotification::BLOOM_M_LOG2 <= 256); + +// this implementation is optimized to not check for packet sizes larger than 24 bytes +const_assert!(MultiSpentNotification::PACKET_SIZE <= 24); + + +struct BloomFilter { + filter: U512, + tx_hash: String, + secret: Vec, +} + +impl BloomFilter { + fn add>( + &mut self, + recipient: &CanonicalAddr, + packet_plaintext: &Vec, + ) -> StdResult> { + // contribute to received bloom filter + let seed = get_seed(&recipient, &self.secret)?; + let id = notification_id(&seed, G::CHANNEL_ID, &self.tx_hash)?; + let hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); + let bloom_mask: U256 = U256::from(G::BLOOM_M - 1); + + // each hash section for up to k times + for i in 0..G::BLOOM_K { + let bit_index = ((hash_bytes >> (256 - G::BLOOM_M_LOG2 - (i * G::BLOOM_M_LOG2))) & bloom_mask).as_usize(); + self.filter |= U512::from(1) << bit_index; + } + + // use top 64 bits of notification ID for packet ID + let packet_id = &id.0.as_slice()[0..8]; - Ok(received_bloom_filter_bytes) + // take the bottom bits from the notification ID for key material + let packet_ikm = &id.0.as_slice()[8..32]; + + // create ciphertext by XOR'ing the plaintext with the notification ID + let packet_ciphertext = xor_bytes( + &packet_plaintext[..], + &packet_ikm[0..G::PACKET_SIZE] + ); + + // construct the packet bytes + let packet_bytes: Vec = [ + packet_id.to_vec(), + packet_ciphertext, + ].concat(); + + Ok(packet_bytes) + } } -pub fn multi_spent_data( + +pub fn render_group_notification>( api: &dyn Api, - notifications: Vec>, + group: G, tx_hash: &String, env_random: Binary, secret: &[u8], -) -> StdResult> { - let mut spent_bloom_filter: U512 = U512::from(0); - let mut spent_packets: Vec<(Addr, Vec)> = vec![]; - - // keep track of how often addresses might show up in packet data. - // we need to remove any address that might show up more than once. - let mut spent_counts: HashMap = HashMap::new(); - - for notification in ¬ifications { - spent_counts.insert( - notification.notification_for.clone(), - spent_counts - .get(¬ification.notification_for) - .unwrap_or(&0u16) - + 1, + resp: Response, +) -> StdResult { + // bloom filter + let mut bloom_filter = BloomFilter { + filter: U512::from(0), + tx_hash: tx_hash.to_string(), + secret: secret.to_vec(), + }; + + // packet structs + let mut packets: Vec<(CanonicalAddr, Vec)> = vec![]; + + // keep track of how many times an address shows up in packet data + let mut recipient_counts: HashMap = HashMap::new(); + + // each notification + for notification in group.notifications() { + // who notification is intended for + let notification_for = api.addr_canonicalize(notification.notification_for.as_str())?; + let notifyee = notification_for.clone(); + + // increment count of recipient occurrence + recipient_counts.insert( + notification_for, + recipient_counts + .get(¬ifyee) + .unwrap_or(&0u16) + 1, ); - // we can short circuit this if recipient count > 1, since we will throw out this packet - // anyway, and address has already been added to bloom filter - if *spent_counts.get(¬ification.notification_for).unwrap() > 1 { + // skip adding this packet if recipient was already seen + if *recipient_counts.get(¬ifyee).unwrap() > 1 { continue; } - let spender_addr_raw = api.addr_canonicalize(notification.notification_for.as_str())?; - let seed = get_seed(&spender_addr_raw, secret)?; - let id = notification_id(&seed, &MULTI_SPENT_CHANNEL_ID.to_string(), &tx_hash)?; - let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); - for _ in 0..MULTI_SPENT_CHANNEL_BLOOM_K { - let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); - spent_bloom_filter = spent_bloom_filter | (U512::from(1) << bit_index); - hash_bytes = hash_bytes >> 9; - } - - // make the spent packet - let mut spent_packet_plaintext: Vec = vec![]; - // amount bytes (u128 == 16 bytes) - spent_packet_plaintext.extend_from_slice(¬ification.data.amount.to_be_bytes()); - // balance bytes (u128 == 16 bytes) - spent_packet_plaintext.extend_from_slice(¬ification.data.balance.to_be_bytes()); - // recipient account last 8 bytes - let recipient_bytes: &[u8]; - let recipient_raw; - if let Some(recipient) = ¬ification.data.recipient { - recipient_raw = api.addr_canonicalize(recipient.as_str())?; - recipient_bytes = &recipient_raw.as_slice()[recipient_raw.0.len() - 8..]; - } else { - recipient_bytes = &ZERO_ADDR[ZERO_ADDR.len() - 8..]; - } - // 40 bytes total - spent_packet_plaintext.extend_from_slice(recipient_bytes); + // build packet + let packet_plaintext = &group.build_packet(api, ¬ification.data)?; - let spent_packet_size = spent_packet_plaintext.len(); - let spent_packet_id = &id.0.as_slice()[0..8]; - let spent_packet_ikm = &id.0.as_slice()[8..32]; - let spent_packet_key = hkdf_sha_512( - &Some(vec![0u8; 64]), - spent_packet_ikm, - "".as_bytes(), - spent_packet_size, + // add to bloom filter + let packet_bytes = bloom_filter.add::( + ¬ifyee, + packet_plaintext, )?; - let spent_packet_ciphertext = xor_bytes( - spent_packet_plaintext.as_slice(), - spent_packet_key.as_slice(), - ); - let spent_packet_bytes: Vec = - [spent_packet_id.to_vec(), spent_packet_ciphertext].concat(); - spent_packets.push((notification.notification_for.clone(), spent_packet_bytes)); + // add to packets data + packets.push((notifyee, packet_bytes)); } - // filter out any notifications for senders showing up more than once - let mut spent_packets: Vec> = spent_packets + // filter out any notifications for recipients showing up more than once + let mut packets: Vec> = packets .into_iter() - .filter(|(addr, _)| *spent_counts.get(addr).unwrap_or(&0u16) <= 1) + .filter(|(addr, _)| *recipient_counts.get(addr).unwrap_or(&0u16) <= 1) .map(|(_, packet)| packet) .collect(); - if spent_packets.len() > MULTI_SPENT_CHANNEL_BLOOM_N as usize { - // still too many packets - spent_packets = spent_packets[0..MULTI_SPENT_CHANNEL_BLOOM_N as usize].to_vec(); + + // still too many packets; trim down to size + if packets.len() > G::BLOOM_N { + packets = packets[0..G::BLOOM_N].to_vec(); } // now add extra packets, if needed, to hide number of packets - let padding_size = - MULTI_SPENT_CHANNEL_BLOOM_N.saturating_sub(spent_packets.len() as u32) as usize; + let padding_size = G::BLOOM_N.saturating_sub(packets.len()); if padding_size > 0 { - let padding_addresses = hkdf_sha_512( + // fill buffer with secure random bytes + let decoy_addresses = hkdf_sha_512( &Some(vec![0u8; 64]), &env_random, - format!("{}:decoys", MULTI_SPENT_CHANNEL_ID).as_bytes(), - padding_size * 20, // 20 bytes per random addr + format!("{}:decoys", G::CHANNEL_ID).as_bytes(), + padding_size * 20, // 20 bytes per random addr )?; // handle each padding package for i in 0..padding_size { - let padding_address = &padding_addresses[i * 20..(i + 1) * 20]; - - // contribute padding packet to bloom filter - let seed = get_seed(&CanonicalAddr::from(padding_address), secret)?; - let id = notification_id(&seed, &MULTI_SPENT_CHANNEL_ID.to_string(), &tx_hash)?; - let mut hash_bytes = U256::from_big_endian(&sha_256(id.0.as_slice())); - for _ in 0..MULTI_SPENT_CHANNEL_BLOOM_K { - let bit_index = (hash_bytes & U256::from(0x01ff)).as_usize(); - spent_bloom_filter = spent_bloom_filter | (U512::from(1) << bit_index); - hash_bytes = hash_bytes >> 9; - } - - // padding packet plaintext - let padding_packet_plaintext = [0u8; MULTI_SPENT_CHANNEL_PACKET_SIZE as usize]; - let padding_plaintext_size = MULTI_SPENT_CHANNEL_PACKET_SIZE as usize; - let padding_packet_id = &id.0.as_slice()[0..8]; - let padding_packet_ikm = &id.0.as_slice()[8..32]; - let padding_packet_key = hkdf_sha_512( - &Some(vec![0u8; 64]), - padding_packet_ikm, - "".as_bytes(), - padding_plaintext_size, - )?; - let padding_packet_ciphertext = xor_bytes( - padding_packet_plaintext.as_slice(), - padding_packet_key.as_slice(), - ); - let padding_packet_bytes: Vec = - [padding_packet_id.to_vec(), padding_packet_ciphertext].concat(); - spent_packets.push(padding_packet_bytes); + // generate address + let address = CanonicalAddr::from(&decoy_addresses[i * 20..(i + 1) * 20]); + + // nil plaintext + let packet_plaintext = vec![0u8; G::PACKET_SIZE]; + + // produce bytes + let packet_bytes = bloom_filter.add::(&address, &packet_plaintext)?; + + // add to packets list + packets.push(packet_bytes); } } - let mut spent_bloom_filter_bytes: Vec = vec![]; - for biguint in spent_bloom_filter.0 { - spent_bloom_filter_bytes.extend_from_slice(&biguint.to_be_bytes()); - } - for packet in spent_packets { - spent_bloom_filter_bytes.extend(packet.iter()); + // prep output bytes + let mut output_bytes: Vec = vec![]; + + // append bloom filter (taking m bottom bits of 512-bit filter) + output_bytes.extend_from_slice( + &bloom_filter.filter.to_big_endian()[((512 - G::BLOOM_M as usize) >> 3)..]); + + // append packets + for packet in packets { + output_bytes.extend(packet.iter()); } - Ok(spent_bloom_filter_bytes) -} \ No newline at end of file + // Ok(output_bytes) + Ok(resp.add_attribute_plaintext( + format!("snip52:#{}", G::CHANNEL_ID), + Binary::from(output_bytes).to_base64())) +} diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 0000000..5241b33 --- /dev/null +++ b/src/query.rs @@ -0,0 +1,573 @@ +use cosmwasm_std::{to_binary, Addr, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Storage, Uint128, Uint64}; +use rand_chacha::ChaChaRng; +use rand_core::{RngCore, SeedableRng}; +use secret_toolkit::notification::{get_seed, notification_id, BloomParameters, ChannelInfoData, Descriptor, DirectChannel, FlatDescriptor, GroupChannel, StructDescriptor}; +use secret_toolkit::permit::{RevokedPermits, RevokedPermitsStore}; + +use crate::{btbe::{find_start_bundle, stored_balance, stored_entry, stored_tx_count}, dwb::{DWB, TX_NODES}, msg::{AllowanceGivenResult, AllowanceReceivedResult, QueryAnswer}, notifications::{AllowanceNotification, MultiRecvdNotification, MultiSpentNotification, RecvdNotification, SpentNotification}, state::{AllowancesStore, MintersStore, CHANNELS, CONFIG, CONTRACT_STATUS, INTERNAL_SECRET_RELAXED, INTERNAL_SECRET_SENSITIVE, TOTAL_SUPPLY}, transaction_history::Tx}; + +pub fn query_exchange_rate(storage: &dyn Storage) -> StdResult { + let constants = CONFIG.load(storage)?; + + if constants.deposit_is_enabled || constants.redeem_is_enabled { + let rate: Uint128; + let denom: String; + // if token has more decimals than SCRT, you get magnitudes of SCRT per token + if constants.decimals >= 6 { + rate = Uint128::new(10u128.pow(constants.decimals as u32 - 6)); + denom = "SCRT".to_string(); + // if token has less decimals, you get magnitudes token for SCRT + } else { + rate = Uint128::new(10u128.pow(6 - constants.decimals as u32)); + denom = constants.symbol; + } + return to_binary(&QueryAnswer::ExchangeRate { rate, denom }); + } + to_binary(&QueryAnswer::ExchangeRate { + rate: Uint128::zero(), + denom: String::new(), + }) +} + +pub fn query_token_info(storage: &dyn Storage) -> StdResult { + let constants = CONFIG.load(storage)?; + + let total_supply = if constants.total_supply_is_public { + Some(Uint128::new(TOTAL_SUPPLY.load(storage)?)) + } else { + None + }; + + to_binary(&QueryAnswer::TokenInfo { + name: constants.name, + symbol: constants.symbol, + decimals: constants.decimals, + total_supply, + }) +} + +pub fn query_token_config(storage: &dyn Storage) -> StdResult { + let constants = CONFIG.load(storage)?; + + to_binary(&QueryAnswer::TokenConfig { + public_total_supply: constants.total_supply_is_public, + deposit_enabled: constants.deposit_is_enabled, + redeem_enabled: constants.redeem_is_enabled, + mint_enabled: constants.mint_is_enabled, + burn_enabled: constants.burn_is_enabled, + supported_denoms: constants.supported_denoms, + }) +} + +pub fn query_contract_status(storage: &dyn Storage) -> StdResult { + let contract_status = CONTRACT_STATUS.load(storage)?; + + to_binary(&QueryAnswer::ContractStatus { + status: contract_status, + }) +} + +pub fn query_transactions( + deps: Deps, + account: String, + page: u32, + page_size: u32, +) -> StdResult { + if page_size == 0 { + return Err(StdError::generic_err("invalid page size")); + } + + // Notice that if query_transactions() was called by a viewing-key call, the address of + // 'account' has already been validated. + // The address of 'account' should not be validated if query_transactions() was called by a + // permit call, for compatibility with non-Secret addresses. + let account = Addr::unchecked(account); + let account_raw = deps.api.addr_canonicalize(account.as_str())?; + + let start = page * page_size; + let mut end = start + page_size; // one more than end index + + // first check if there are any transactions in dwb + let dwb = DWB.load(deps.storage)?; + let dwb_index = dwb.recipient_match(&account_raw); + let mut txs_in_dwb = vec![]; + let txs_in_dwb_count = dwb.entries[dwb_index].list_len()?; + if dwb_index > 0 && txs_in_dwb_count > 0 && start < txs_in_dwb_count as u32 { + // skip if start is after buffer entries + let head_node_index = dwb.entries[dwb_index].head_node()?; + + // only look if head node is not null + if head_node_index > 0 { + let head_node = TX_NODES + .add_suffix(&head_node_index.to_be_bytes()) + .load(deps.storage)?; + txs_in_dwb = head_node.to_vec(deps.storage, deps.api)?; + } + } + + //let account_slice = account_raw.as_slice(); + let account_stored_entry = stored_entry(deps.storage, &account_raw)?; + let settled_tx_count = stored_tx_count(deps.storage, &account_stored_entry)?; + let total = txs_in_dwb_count as u32 + settled_tx_count as u32; + if end > total { + end = total; + } + + let mut txs: Vec = vec![]; + + let txs_in_dwb_count = txs_in_dwb_count as u32; + if start < txs_in_dwb_count && end < txs_in_dwb_count { + // option 1, start and end are both in dwb + //println!("OPTION 1"); + txs = txs_in_dwb[start as usize..end as usize].to_vec(); // reverse chronological + } else if start < txs_in_dwb_count && end >= txs_in_dwb_count { + // option 2, start is in dwb and end is in settled txs + // in this case, we do not need to search for txs, just begin at last bundle and move backwards + //println!("OPTION 2"); + txs = txs_in_dwb[start as usize..].to_vec(); // reverse chronological + let mut txs_left = (end - start).saturating_sub(txs.len() as u32); + if let Some(entry) = account_stored_entry { + let tx_bundles_idx_len = entry.history_len()?; + if tx_bundles_idx_len > 0 { + let mut bundle_idx = tx_bundles_idx_len - 1; + loop { + let tx_bundle = entry.get_tx_bundle_at(deps.storage, bundle_idx.clone())?; + + // only look if head node is not null + if tx_bundle.head_node > 0 { + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + + let list_len = tx_bundle.list_len as u32; + if txs_left <= list_len { + txs.extend_from_slice( + &head_node.to_vec(deps.storage, deps.api)?[0..txs_left as usize], + ); + break; + } + txs.extend(head_node.to_vec(deps.storage, deps.api)?); + txs_left = txs_left.saturating_sub(list_len); + } + if bundle_idx > 0 { + bundle_idx -= 1; + } else { + break; + } + } + } + } + } else if start >= txs_in_dwb_count { + // option 3, start is not in dwb + // in this case, search for where the beginning bundle is using binary search + + // bundle tx offsets are chronological, but we need reverse chronological + // so get the settled start index as if order is reversed + //println!("OPTION 3"); + let settled_start = settled_tx_count + .saturating_sub(start - txs_in_dwb_count) + .saturating_sub(1); + + if let Some((bundle_idx, tx_bundle, start_at)) = + find_start_bundle(deps.storage, &account_raw, settled_start)? + { + let mut txs_left = end - start; + let list_len = tx_bundle.list_len as u32; + if start_at + txs_left <= list_len { + // only look if head node is not null + if tx_bundle.head_node > 0 { + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + // this first bundle has all the txs we need + txs = head_node.to_vec(deps.storage, deps.api)? + [start_at as usize..(start_at + txs_left) as usize] + .to_vec(); + } + } else { + // only look if head node is not null + if tx_bundle.head_node > 0 { + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + // get the rest of the txs in this bundle and then go back through history + txs = head_node.to_vec(deps.storage, deps.api)?[start_at as usize..].to_vec(); + txs_left = txs_left.saturating_sub(list_len - start_at); + } + + if bundle_idx > 0 && txs_left > 0 { + // get the next earlier bundle + let mut bundle_idx = bundle_idx - 1; + if let Some(entry) = account_stored_entry { + loop { + let tx_bundle = + entry.get_tx_bundle_at(deps.storage, bundle_idx.clone())?; + // only look if head node is not null + if tx_bundle.head_node > 0 { + let head_node = TX_NODES + .add_suffix(&tx_bundle.head_node.to_be_bytes()) + .load(deps.storage)?; + let list_len = tx_bundle.list_len as u32; + if txs_left <= list_len { + txs.extend_from_slice( + &head_node.to_vec(deps.storage, deps.api)? + [0..txs_left as usize], + ); + break; + } + txs.extend(head_node.to_vec(deps.storage, deps.api)?); + txs_left = txs_left.saturating_sub(list_len); + } + if bundle_idx > 0 { + bundle_idx -= 1; + } else { + break; + } + } + } + } + } + } + } + + // deterministically obfuscate ids so they are not serial to prevent metadata leak + let internal_secret = INTERNAL_SECRET_RELAXED.load(deps.storage)?; + let internal_secret_u64: u64 = u64::from_be_bytes(internal_secret[..8].try_into().unwrap()); + let txs = txs + .iter() + .map(|tx| { + // PRNG(PRNG(serial_id) ^ secret) + let mut rng = ChaChaRng::seed_from_u64(tx.id); + let serial_id_rand = rng.next_u64(); + let new_seed = serial_id_rand ^ internal_secret_u64; + let mut rng = ChaChaRng::seed_from_u64(new_seed); + let new_id = rng.next_u64() >> (64 - 53); + Tx { + id: new_id, + action: tx.action.clone(), + coins: tx.coins.clone(), + memo: tx.memo.clone(), + block_height: tx.block_height, + block_time: tx.block_time, + } + }) + .collect(); + + let result = QueryAnswer::TransactionHistory { + txs, + total: Some(total as u64), + }; + to_binary(&result) +} + +pub fn query_balance(deps: Deps, account: String) -> StdResult { + // Notice that if query_balance() was called by a viewing key call, the address of 'account' + // has already been validated. + // The address of 'account' should not be validated if query_balance() was called by a permit + // call, for compatibility with non-Secret addresses. + let account = Addr::unchecked(account); + let account = deps.api.addr_canonicalize(account.as_str())?; + + let mut amount = stored_balance(deps.storage, &account)?; + let dwb = DWB.load(deps.storage)?; + let dwb_index = dwb.recipient_match(&account); + if dwb_index > 0 { + amount = amount.saturating_add(dwb.entries[dwb_index].amount()? as u128); + } + let amount = Uint128::new(amount); + let response = QueryAnswer::Balance { amount }; + to_binary(&response) +} + +pub fn query_minters(deps: Deps) -> StdResult { + let minters = MintersStore::load(deps.storage)?; + + let response = QueryAnswer::Minters { minters }; + to_binary(&response) +} + +pub fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult { + // Notice that if query_allowance() was called by a viewing-key call, the addresses of 'owner' + // and 'spender' have already been validated. + // The addresses of 'owner' and 'spender' should not be validated if query_allowance() was + // called by a permit call, for compatibility with non-Secret addresses. + let owner = Addr::unchecked(owner); + let spender = Addr::unchecked(spender); + + let allowance = AllowancesStore::load(deps.storage, &owner, &spender); + + let response = QueryAnswer::Allowance { + owner, + spender, + allowance: Uint128::new(allowance.amount), + expiration: allowance.expiration, + }; + to_binary(&response) +} + +pub fn query_allowances_given( + deps: Deps, + owner: String, + page: u32, + page_size: u32, +) -> StdResult { + // Notice that if query_all_allowances_given() was called by a viewing-key call, + // the address of 'owner' has already been validated. + // The addresses of 'owner' should not be validated if query_all_allowances_given() was + // called by a permit call, for compatibility with non-Secret addresses. + let owner = Addr::unchecked(owner); + + let all_allowances = + AllowancesStore::all_allowances(deps.storage, &owner, page, page_size).unwrap_or(vec![]); + + let allowances_result = all_allowances + .into_iter() + .map(|(spender, allowance)| AllowanceGivenResult { + spender, + allowance: Uint128::from(allowance.amount), + expiration: allowance.expiration, + }) + .collect(); + + let response = QueryAnswer::AllowancesGiven { + owner: owner.clone(), + allowances: allowances_result, + count: AllowancesStore::num_allowances(deps.storage, &owner), + }; + to_binary(&response) +} + +pub fn query_allowances_received( + deps: Deps, + spender: String, + page: u32, + page_size: u32, +) -> StdResult { + // Notice that if query_all_allowances_received() was called by a viewing-key call, + // the address of 'spender' has already been validated. + // The addresses of 'spender' should not be validated if query_all_allowances_received() was + // called by a permit call, for compatibility with non-Secret addresses. + let spender = Addr::unchecked(spender); + + let all_allowed = + AllowancesStore::all_allowed(deps.storage, &spender, page, page_size).unwrap_or(vec![]); + + let allowances = all_allowed + .into_iter() + .map(|(owner, allowance)| AllowanceReceivedResult { + owner, + allowance: Uint128::from(allowance.amount), + expiration: allowance.expiration, + }) + .collect(); + + let response = QueryAnswer::AllowancesReceived { + spender: spender.clone(), + allowances, + count: AllowancesStore::num_allowed(deps.storage, &spender), + }; + to_binary(&response) +} + +// ***************** +// SNIP-24.1 query function +// ***************** + +pub fn query_list_permit_revocations(deps: Deps, account: &str) -> StdResult { + let revocations = RevokedPermits::list_revocations( + deps.storage, + account + )?; + + to_binary(&QueryAnswer::ListPermitRevocations { revocations }) +} + +// ***************** +// SNIP-52 query functions +// ***************** + +/// +/// ListChannels query +/// +/// Public query to list all notification channels. +/// +pub fn query_list_channels(deps: Deps) -> StdResult { + let channels: Vec = CHANNELS + .iter(deps.storage)? + .map(|channel| channel.unwrap()) + .collect(); + to_binary(&QueryAnswer::ListChannels { channels }) +} + +/// +/// ChannelInfo query +/// +/// Authenticated query allows clients to obtain the seed, +/// and Notification ID of an event for a specific tx_hash, for a specific channel. +/// +pub fn query_channel_info( + deps: Deps, + env: Env, + channels: Vec, + txhash: Option, + sender_raw: CanonicalAddr, +) -> StdResult { + let secret = INTERNAL_SECRET_SENSITIVE.load(deps.storage)?; + let secret = secret.as_slice(); + let seed = get_seed(&sender_raw, secret)?; + let mut channels_data = vec![]; + for channel in channels { + let answer_id; + if let Some(tx_hash) = &txhash { + answer_id = Some(notification_id(&seed, &channel, tx_hash)?); + } else { + answer_id = None; + } + match channel.as_str() { + RecvdNotification::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "txhash".to_string(), + channel, + answer_id, + parameters: None, + data: None, + next_id: None, + counter: None, + cddl: Some(RecvdNotification::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + SpentNotification::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "txhash".to_string(), + channel, + answer_id, + parameters: None, + data: None, + next_id: None, + counter: None, + cddl: Some(SpentNotification::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + AllowanceNotification::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "txhash".to_string(), + channel, + answer_id, + parameters: None, + data: None, + next_id: None, + counter: None, + cddl: Some(AllowanceNotification::CDDL_SCHEMA.to_string()), + }; + channels_data.push(channel_info_data); + } + MultiRecvdNotification::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "bloom".to_string(), + channel, + answer_id, + parameters: Some(BloomParameters { + m: MultiRecvdNotification::BLOOM_M, + k: MultiRecvdNotification::BLOOM_K, + h: "sha256".to_string(), + }), + data: Some(Descriptor { + r#type: format!("packet[{}]", MultiRecvdNotification::BLOOM_N), + version: "1".to_string(), + packet_size: MultiRecvdNotification::PACKET_SIZE as u32, + data: StructDescriptor { + r#type: "struct".to_string(), + label: "transfer".to_string(), + members: vec![ + FlatDescriptor { + r#type: "uint64".to_string(), + label: "flagsAndAmount".to_string(), + description: Some( + "Bit field of [0]: non-empty memo; [2]: sender is owner; [2..]: uint62 transfer amount in base denomination".to_string(), + ), + }, + FlatDescriptor { + r#type: "bytes8".to_string(), + label: "ownerId".to_string(), + description: Some( + "The last 8 bytes of the owner's canonical address".to_string(), + ), + }, + ], + }, + }), + counter: None, + next_id: None, + cddl: None, + }; + channels_data.push(channel_info_data); + } + MultiSpentNotification::CHANNEL_ID => { + let channel_info_data = ChannelInfoData { + mode: "bloom".to_string(), + channel, + answer_id, + parameters: Some(BloomParameters { + m: MultiSpentNotification::BLOOM_M, + k: MultiSpentNotification::BLOOM_K, + h: "sha256".to_string(), + }), + data: Some(Descriptor { + r#type: format!("packet[{}]", MultiSpentNotification::BLOOM_N), + version: "1".to_string(), + packet_size: MultiSpentNotification::PACKET_SIZE as u32, + data: StructDescriptor { + r#type: "struct".to_string(), + label: "transfer".to_string(), + members: vec![ + FlatDescriptor { + r#type: "uint64".to_string(), + label: "flagsAndAmount".to_string(), + description: Some( + "Bit field of [0]: non-empty memo; [1]: reserved; [2..] uint62 transfer amount in base denomination".to_string(), + ), + }, + FlatDescriptor { + r#type: "bytes8".to_string(), + label: "recipientId".to_string(), + description: Some( + "The last 8 bytes of the recipient's canonical address".to_string(), + ), + }, + FlatDescriptor { + r#type: "uint64".to_string(), + label: "balance".to_string(), + description: Some( + "Spender's new balance after the transfer".to_string(), + ), + }, + ], + }, + }), + counter: None, + next_id: None, + cddl: None, + }; + channels_data.push(channel_info_data); + } + _ => { + return Err(StdError::generic_err(format!( + "`{}` channel is undefined", + channel + ))); + } + } + } + + to_binary(&QueryAnswer::ChannelInfo { + as_of_block: Uint64::from(env.block.height), + channels: channels_data, + seed, + }) +} + +// ***************** +// End SNIP-52 query functions +// ***************** diff --git a/src/state.rs b/src/state.rs index 3c44b4f..5dcea93 100644 --- a/src/state.rs +++ b/src/state.rs @@ -222,8 +222,14 @@ impl ReceiverHashStore { } } -pub static INTERNAL_SECRET: Item> = Item::new(b"internal-secret"); +/// internal secret used for sensitive data such as address hashes in the btbe and notifications +pub static INTERNAL_SECRET_SENSITIVE: Item> = Item::new(b"internal-secret-secure"); -// SNIP-52 channels +/// internal secret used for less sensitive data such as obfuscating tx IDs +pub static INTERNAL_SECRET_RELAXED: Item> = Item::new(b"internal-secret-prng"); + +/// SNIP-52 channels pub static CHANNELS: Keyset = Keyset::new(b"channel-ids"); +/// SNIP-52 status +pub static NOTIFICATIONS_ENABLED: Item = Item::new(b"notify-status"); \ No newline at end of file diff --git a/src/strings.rs b/src/strings.rs index 71a574b..a456db1 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1,2 +1,4 @@ pub const TRANSFER_HISTORY_UNSUPPORTED_MSG: &str = "`transfer_history` query is UNSUPPORTED. Use `transaction_history` instead."; +pub const SEND_TO_CONTRACT_ERR_MSG: &str = + "Tokens cannot be sent to token contract."; \ No newline at end of file