diff --git a/Cargo.lock b/Cargo.lock index c80002d..439400b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,9 +81,9 @@ checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" [[package]] name = "cosmwasm-crypto" -version = "1.1.9" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227315dc11f0bb22a273d0c43d3ba8ef52041c42cf959f09045388a89c57e661" +checksum = "75836a10cb9654c54e77ee56da94d592923092a10b369cdb0dbd56acefc16340" dependencies = [ "digest 0.10.6", "ed25519-zebra", @@ -94,11 +94,11 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.1.9" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fca30d51f7e5fbfa6440d8b10d7df0231bdf77e97fd3fe5d0cb79cc4822e50c" +checksum = "1c9f7f0e51bfc7295f7b2664fe8513c966428642aa765dad8a74acdab5e0c773" dependencies = [ - "syn", + "syn 1.0.105", ] [[package]] @@ -122,14 +122,14 @@ checksum = "a06c8f516a13ae481016aa35f0b5c4652459e8aee65b15b6fb51547a07cea5a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] name = "cosmwasm-std" -version = "1.1.9" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13d5a84d15cf7be17dc249a21588cdb0f7ef308907c50ce2723316a7d79c3dc" +checksum = "a49b85345e811c8e80ec55d0d091e4fcb4f00f97ab058f9be5f614c444a730cb" dependencies = [ "base64", "cosmwasm-crypto", @@ -140,20 +140,11 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", + "sha2 0.10.6", "thiserror", "uint", ] -[[package]] -name = "cosmwasm-storage" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9162c3f85412914d5be2ee650ebdbf11b08e5e9acdebcf4dc03608fb01cf9676" -dependencies = [ - "cosmwasm-std", - "serde", -] - [[package]] name = "cpufeatures" version = "0.2.5" @@ -205,34 +196,42 @@ dependencies = [ ] [[package]] -name = "cw-multi-test" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e81b4a7821d5eeba0d23f737c16027b39a600742ca8c32eb980895ffd270f4" +name = "cw-ics721-governance" +version = "0.0.1" dependencies = [ - "anyhow", + "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "derivative", - "itertools", - "prost", + "cw-ics721-governance-derive", + "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", + "cw721", + "cw721-base", + "cw721-proxy", "schemars", "serde", "thiserror", ] +[[package]] +name = "cw-ics721-governance-derive" +version = "0.0.1" +dependencies = [ + "cosmwasm-std", + "proc-macro2", + "quote", + "syn 1.0.105", +] + [[package]] name = "cw-multi-test" -version = "0.16.1" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc50fde3ad87ef4e3a3e57c73d11326333318761c7655cc8cae67c40382ac91" +checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042" dependencies = [ "anyhow", "cosmwasm-std", - "cw-storage-plus 0.16.0", - "cw-utils 0.16.0", + "cw-storage-plus 1.0.1", + "cw-utils 1.0.1", "derivative", "itertools", "k256", @@ -254,17 +253,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cw-storage-plus" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" -dependencies = [ - "cosmwasm-std", - "schemars", - "serde", -] - [[package]] name = "cw-storage-plus" version = "0.16.0" @@ -277,18 +265,14 @@ dependencies = [ ] [[package]] -name = "cw-utils" -version = "0.15.1" +name = "cw-storage-plus" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" +checksum = "053a5083c258acd68386734f428a5a171b29f7d733151ae83090c6fcc9417ffa" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", - "cw2 0.15.1", "schemars", - "semver", "serde", - "thiserror", ] [[package]] @@ -307,16 +291,18 @@ dependencies = [ ] [[package]] -name = "cw2" -version = "0.15.1" +name = "cw-utils" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5abb8ecea72e09afff830252963cb60faf945ce6cef2c20a43814516082653da" +checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", + "cw2 1.0.1", "schemars", + "semver", "serde", + "thiserror", ] [[package]] @@ -333,14 +319,14 @@ dependencies = [ ] [[package]] -name = "cw721" -version = "0.15.0" +name = "cw2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20dfe04f86e5327956b559ffcc86d9a43167391f37402afd8bf40b0be16bee4d" +checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils 0.15.1", + "cw-storage-plus 1.0.1", "schemars", "serde", ] @@ -369,12 +355,152 @@ dependencies = [ "cw-storage-plus 0.16.0", "cw-utils 0.16.0", "cw2 0.16.0", - "cw721 0.16.0", + "cw721", "schemars", "serde", "thiserror", ] +[[package]] +name = "cw721-governed-channel-proxy" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ics721-governance", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw721", + "cw721-base", + "cw721-governed-proxy", + "cw721-proxy", + "cw721-proxy-derive", + "cw721-proxy-multi-test", + "cw721-proxy-tester", + "cw721-whitelist", + "ibc-outgoing-msg", + "rand", + "thiserror", +] + +[[package]] +name = "cw721-governed-collection-channels-proxy" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ics721-governance", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw721", + "cw721-base", + "cw721-governed-proxy", + "cw721-proxy", + "cw721-proxy-derive", + "cw721-proxy-multi-test", + "cw721-proxy-tester", + "cw721-whitelist-map", + "ibc-outgoing-msg", + "rand", + "thiserror", +] + +[[package]] +name = "cw721-governed-collection-proxy" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ics721-governance", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw721", + "cw721-base", + "cw721-governed-proxy", + "cw721-proxy", + "cw721-proxy-derive", + "cw721-proxy-multi-test", + "cw721-proxy-tester", + "cw721-whitelist", + "rand", + "thiserror", +] + +[[package]] +name = "cw721-governed-proxy" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ics721-governance", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw721", + "cw721-base", + "cw721-proxy", + "cw721-proxy-multi-test", + "cw721-proxy-tester", + "ibc-outgoing-msg", + "thiserror", +] + +[[package]] +name = "cw721-governed-rate-limited-proxy" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ics721-governance", + "cw-multi-test", + "cw-rate-limiter", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw721", + "cw721-base", + "cw721-governed-proxy", + "cw721-proxy", + "cw721-proxy-derive", + "cw721-proxy-multi-test", + "cw721-proxy-tester", + "ibc-outgoing-msg", + "rand", + "thiserror", +] + +[[package]] +name = "cw721-governed_code-id-proxy" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ics721-governance", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw721", + "cw721-base", + "cw721-governed-proxy", + "cw721-proxy", + "cw721-proxy-derive", + "cw721-proxy-multi-test", + "cw721-proxy-tester", + "cw721-whitelist", + "ibc-outgoing-msg", + "rand", + "thiserror", +] + [[package]] name = "cw721-proxy" version = "0.0.1" @@ -382,7 +508,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.16.0", - "cw721 0.16.0", + "cw721", "cw721-proxy-derive", "thiserror", ] @@ -391,10 +517,26 @@ dependencies = [ name = "cw721-proxy-derive" version = "0.0.1" dependencies = [ - "cw721 0.16.0", + "cw721", "proc-macro2", "quote", - "syn", + "syn 1.0.105", +] + +[[package]] +name = "cw721-proxy-multi-test" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ics721-governance", + "cw-multi-test", + "cw-utils 0.16.0", + "cw721", + "cw721-base", + "cw721-proxy-tester", + "ibc-outgoing-msg", ] [[package]] @@ -403,10 +545,10 @@ version = "0.0.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.15.1", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw721 0.15.0", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw721", "cw721-proxy", "cw721-proxy-derive", "thiserror", @@ -419,11 +561,11 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.16.1", + "cw-multi-test", "cw-rate-limiter", "cw-storage-plus 0.16.0", "cw2 0.16.0", - "cw721 0.16.0", + "cw721", "cw721-base", "cw721-proxy", "cw721-proxy-derive", @@ -432,6 +574,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw721-whitelist" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.16.0", + "schemars", + "thiserror", +] + +[[package]] +name = "cw721-whitelist-map" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.16.0", + "schemars", + "thiserror", +] + [[package]] name = "der" version = "0.6.1" @@ -450,7 +614,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -604,6 +768,14 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "ibc-outgoing-msg" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", +] + [[package]] name = "itertools" version = "0.10.5" @@ -667,9 +839,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "c4ec6d5fe0b140acb27c9a0444118cf55bfbb4e0b259739429abb4521dd67c16" dependencies = [ "unicode-ident", ] @@ -694,14 +866,14 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -780,7 +952,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 1.0.105", ] [[package]] @@ -814,9 +986,9 @@ dependencies = [ [[package]] name = "serde-json-wasm" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479b4dbc401ca13ee8ce902851b834893251404c4f3c65370a49e047a6be09a5" +checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" dependencies = [ "serde", ] @@ -829,7 +1001,7 @@ checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -840,7 +1012,7 @@ checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -921,24 +1093,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.16", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 38f3cd3..b653f8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,43 @@ [workspace] -members = ["contracts/*", "packages/*", "debug/*"] +members = ["contracts/*", "packages/*", "debug/*"] + +[workspace.package] +authors = ["Mr T "] +edition = "2021" +homepage = "https://arkprotocol.io" +repository = "https://github.com/arkprotocol/cw721-proxy/" +license = "Apache-2.0" +keywords = ["cosmos", "cosmwasm"] + +[workspace.dependencies] +cosmwasm-std = "1.1" +cosmwasm-schema = "1.1" +cw-storage-plus = "0.16" +cw2 = "0.16" +cw721 = "0.16" +cw721-proxy = { path = "./packages/cw721-proxy", version = "*" } +cw721-proxy-derive = { path = "./packages/cw721-proxy-derive", version = "*" } +cw721-whitelist = { path = "./packages/cw721-whitelist", version = "*" } +cw721-whitelist-map = { path = "./packages/cw721-whitelist-map", version = "*" } +cw721-governed-proxy = { path = "./contracts/cw721-governed-proxy", version = "*" } +cw-ics721-governance = { path = "./packages/cw-ics721-governance", version = "*" } +cw-ics721-governance-derive = { path = "./packages/cw-ics721-governance/derive", version = "*" } +cw-rate-limiter = { path = "./packages/cw-rate-limiter", version = "*" } +cw-utils = "0.16.0" +ibc-outgoing-msg = { path = "./packages/ibc-outgoing-msg", version = "*" } +cw721-proxy-multi-test = { path = "./packages/cw721-proxy-multi-test", version = "*" } +schemars = "0.8.11" +proc-macro2 = "1.0" +quote = "1.0" +serde = "1.0" +syn = { version = "1.0", features = ["derive"] } +thiserror = "1" +# dev dependencies +cw-multi-test = "0.16.4" +cw721-base = "0.16" +cw721-proxy-tester = { path = "./debug/cw721-proxy-tester", version = "*" } +rand = "0.8" +anyhow = "1.0" [profile.release] codegen-units = 1 diff --git a/README.md b/README.md index 2accc03..8234a95 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,98 @@ # CW 721 Proxy -1. `packages/cw721-proxy` An interface for proxying cw721 send messages. -3. `contracts/cw721-rate-limited-proxy` An implementation of this +1. `contracts/cw721-rate-limited-proxy` An implementation of this proxy interface to rate limit incoming cw721 send messages. -2. `packages/cw-rate-limiter` package for rate limiting in CosmWasm +2. `contracts/cw721-governed-proxy` A a governed proxy with an optional transfer fee + being eligible to `bridge_nft`s to origin contract. +3. `contracts/cw721-governed-rate-limited-proxy` A `cw721-governed-proxy` extension of above rate limited proxy. +4. `contracts/cw721-governed-channel-proxy` A `cw721-governed-proxy` extension with a channel whitelist + being eligible to `send_nft`s to origin contract. +5. `contracts/cw721-governed-code-id-proxy` A `cw721-governed-proxy` extension with a code id whitelist + being eligible to `send_nft`s to origin contract. +6. `contracts/cw721-governed-collection-proxy` A `cw721-governed-proxy` extension with a collection (cw721/sender) whitelist + being eligible to `send_nft`s to origin contract. +7. `contracts/cw721-governed-collection-channels-proxy` A `cw721-governed-proxy` extension with a sender and channels whitelist + being eligible to `send_nft`s to origin contract. +8. `packages/cw721-proxy` An interface for proxying cw721 send messages. +9. `packages/cw-rate-limiter` package for rate limiting in CosmWasm contracts. -4. `packages/cw721-proxy-derive` Procedural macros for deriving the +10. `packages/cw721-proxy-derive` Procedural macros for deriving the proxy receiver message types on an existing enum. +11. `packages/cw-governed-proxy-multitest` Test app with helper functions for testing governed proxies. +12. `packages/cw721-whitelist` An `Item<'a, Vec>` store for whitelisting in CosmWasm contracts. +13. `packages/cw721-whitelist-map` A `Map<'a, K, T>` store for whitelisting in CosmWasm contracts. + contracts. +14. `packages/ibc-outgoing-msg` An `IbcOutgoingMsg` struct, required by [ICS721](https://github.com/public-awesome/ics721/blob/main/contracts/cw-ics721-bridge/src/msg.rs#L84-L95). + +# Proxies + +## Rate Limited Proxy + +A simple proxy to rate limit incoming cw721 send messages. A rate limit can be set based on `Blocks` or `PerBlocks` as defined in [Rate](.packages/cw-rate-limiter/src/lib.rs#L15). + +An incoming `receive_nft` message is forwarded to a given `origin` (ics721 contract). + +## Governed Proxy + +A simple governed proxy. There are 2 possibilities NFTs can be transferred to another chain: + +1. `receive_nft`: NFT is send from collection to proxy. Proxy gets a `receive_nft` from collection, transfer ownership to ics721 (escrowed) and __forwards receive msg__ to ics721, which then handles inter-chain transfer. +2. `bridge_nft`: NFT is send by calling `bridge_nft` directly on proxy. Proxy is allowed and have approval to transfer nft to ics721 and __sends a receive msg__ to ics721, which then handles inter-chain transfer. + +A governed proxy stores this: + +- `origin`: required ics721 contract for handling inter-chain transfers. +- `owner`: optional, if given only owner is authorized changing `origin`, `owner`, `transfer_fee` and execute `send_funds`. +- `transfer_fee`: optional, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds! + +IMPORTANT: in case `owner` is not set, above stores (`origin`, `owner` and `transfer_fee`) are immutable. Also `send_funds` is disabled then. The only way of changing these is via migration! + +## Governed Rate Limited Proxy + +A `cw721-governed-proxy` extension of above rate limited proxy. + +## Governed Channel Proxy + +A `cw721-governed-proxy` extension with a channel whitelist being eligible to `send_nft`s to origin contract. + +## Governed Code Id Proxy + +A `cw721-governed-proxy` extension with a code id whitelist being eligible to `send_nft`s to origin contract. + +## Governed Collection Proxy + +A `cw721-governed-proxy` extension with a collection (cw721/sender) whitelist being eligible to `send_nft`s to origin contract. + +## Governed Collection Channels Proxy + +A `cw721-governed-proxy` extension with a sender and channels whitelist being eligible to `send_nft`s to origin contract. + +# Packages + +## cw721-proxy + +An interface for proxying cw721 send messages. + +## cw-rate-limiter + +Package for rate limiting in CosmWasm contracts. + +## cw721-proxy-derive + +Procedural macros for deriving the proxy receiver message types on an existing enum. + +## cw-governed-proxy-multitest + +Test app with helper functions for testing governed proxies. + +## cw721-whitelist + +An `Item<'a, Vec>` store for whitelisting in CosmWasm contracts. + +## cw721-whitelist-map + +A `Map<'a, K, T>` store for whitelisting in CosmWasm contracts. + +## ibc-outgoing-msg + +An `IbcOutgoingMsg` struct, required by [ICS721](https://github.com/public-awesome/ics721/blob/main/contracts/cw-ics721-bridge/src/msg.rs#L84-L95). \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0ccc86c --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +## Compiles and optimizes contracts + +set -o errexit -o nounset -o pipefail +command -v shellcheck >/dev/null && shellcheck "$0" + +cd "$(git rev-parse --show-toplevel)" + +docker run --rm -v "$(pwd)":/code --platform linux/amd64 \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/workspace-optimizer:0.12.9 + +ls -al ./artifacts/*wasm \ No newline at end of file diff --git a/contracts/cw721-governed-channel-proxy/.cargo/config b/contracts/cw721-governed-channel-proxy/.cargo/config new file mode 100644 index 0000000..af5698e --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/cw721-governed-channel-proxy/.gitignore b/contracts/cw721-governed-channel-proxy/.gitignore new file mode 100644 index 0000000..dfdaaa6 --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/cw721-governed-channel-proxy/Cargo.toml b/contracts/cw721-governed-channel-proxy/Cargo.toml new file mode 100644 index 0000000..35866c8 --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "cw721-governed-channel-proxy" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-proxy = { workspace = true } +cw721-proxy-derive = { workspace = true } +cw721-whitelist = { workspace = true } +cw721-governed-proxy = { workspace = true, features = ["library"] } +cw-ics721-governance = { workspace = true} +ibc-outgoing-msg = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-base = { workspace = true } +cw721-proxy-tester = { workspace = true } +cw721-proxy-multi-test = { workspace = true } +rand = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/cw721-governed-channel-proxy/schema/cw721-governed-channel-proxy.json b/contracts/cw721-governed-channel-proxy/schema/cw721-governed-channel-proxy.json new file mode 100644 index 0000000..e5502cf --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/schema/cw721-governed-channel-proxy.json @@ -0,0 +1,439 @@ +{ + "contract_name": "cw721-governed-channel-proxy", + "contract_version": "0.0.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "properties": { + "origin": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "whitelist": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "add_to_whitelist" + ], + "properties": { + "add_to_whitelist": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_from_whitelist" + ], + "properties": { + "remove_from_whitelist": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "clear_whitelist" + ], + "properties": { + "clear_whitelist": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "description": "Actions that can be taken to alter the proxy contract's governance like in `execute`'s entry point: ```rust use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; use cosmwasm_schema::cw_serde; use cosmwasm_std::entry_point;\n\n#[cw_serde] pub enum ExecuteMsg { Governance(cw_ics721_governance::Action), ReceiveNft(cw721::Cw721ReceiveMsg) }\n\n#[cfg_attr(not(feature = \"library\"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::Governance(action) => { Ok(cw_ics721_governance::execute(deps, env, &info, action)?) } ExecuteMsg::ReceiveNft(msg) => { Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) } } } ```", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Cw721ReceiveMsg to be forwared to ICS721 (origin). NOTE: this is NOT part of governance, since it is send by cw721 contract diretly to proxy", + "type": "object", + "required": [ + "receive_nft" + ], + "properties": { + "receive_nft": { + "$ref": "#/definitions/Cw721ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the proxy contract's governance", + "oneOf": [ + { + "description": "Changing owner of proxy. Once set, it can't be set to None - except via migration.", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "ICS721 contract where Cw721ReceiveMsg is forwarded to.", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds!", + "type": "object", + "required": [ + "transfer_fee" + ], + "properties": { + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "description": "Send funds from proxy to specified address.", + "type": "object", + "required": [ + "send_funds" + ], + "properties": { + "send_funds": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721.", + "type": "object", + "required": [ + "bridge_nft" + ], + "properties": { + "bridge_nft": { + "type": "object", + "required": [ + "collection", + "msg", + "token_id" + ], + "properties": { + "collection": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Cw721ReceiveMsg": { + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "msg", + "sender", + "token_id" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "whitelist" + ], + "properties": { + "whitelist": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's governance information", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "governance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Governance", + "description": "A governed contract may have: - an optional owner, - an origin (ICS721) where msgs are forwarded to, and - an optional transfer fee.\n\nOwner: - used in assert_owner(). For example execute_transfer_fee() allows only owner to change fees.\n\nOrigin: - ...\n\nTransfer Fee: - ...", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "$ref": "#/definitions/Addr" + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "whitelist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + }, + "whitelisted": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + } + } +} diff --git a/contracts/cw721-governed-channel-proxy/src/bin/schema.rs b/contracts/cw721-governed-channel-proxy/src/bin/schema.rs new file mode 100644 index 0000000..a3f1b25 --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw721_governed_channel_proxy::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/cw721-governed-channel-proxy/src/error.rs b/contracts/cw721-governed-channel-proxy/src/error.rs new file mode 100644 index 0000000..987847a --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/src/error.rs @@ -0,0 +1,15 @@ +use cosmwasm_std::StdError; +use cw_ics721_governance::GovernanceError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Governance(#[from] GovernanceError), + + #[error("{requestee} not whitelisted!")] + NotWhitelisted { requestee: String }, +} diff --git a/contracts/cw721-governed-channel-proxy/src/lib.rs b/contracts/cw721-governed-channel-proxy/src/lib.rs new file mode 100644 index 0000000..3d24374 --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/src/lib.rs @@ -0,0 +1,189 @@ +use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; +use cw721::Cw721ReceiveMsg; +use error::ContractError; +use ibc_outgoing_msg::IbcOutgoingMsg; +use state::WHITELIST; + +pub mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +#[cfg(not(feature = "library"))] +pub mod entry { + use crate::error::ContractError; + use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + use crate::state::{CONTRACT_NAME, CONTRACT_VERSION, WHITELIST}; + use crate::{ + execute_add_to_whitelist, execute_bridge_nft, execute_clear_whitelist, execute_receive_nft, + execute_remove_from_whitelist, + }; + + use cosmwasm_std::{entry_point, to_binary}; + use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + use cw2::set_contract_version; + use cw_ics721_governance::Action; + + // This makes a conscious choice on the various generics used by the contract + #[entry_point] + pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + WHITELIST.init(deps.storage, &msg.whitelist)?; + let res = + cw_ics721_governance::instantiate(deps, info, msg.owner, msg.origin, msg.transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + msg.whitelist + .map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + + #[entry_point] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + match msg { + ExecuteMsg::Governance(Action::BridgeNft { + collection, + token_id, + msg, + }) => execute_bridge_nft(deps, env, info, collection, token_id, msg), + ExecuteMsg::Governance(action) => { + Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + } + ExecuteMsg::ReceiveNft(msg) => execute_receive_nft(deps, env, info, msg), + ExecuteMsg::AddToWhitelist { value } => { + execute_add_to_whitelist(deps, env, info, &value) + } + ExecuteMsg::RemoveFromWhitelist { value } => { + execute_remove_from_whitelist(deps, env, info, &value) + } + ExecuteMsg::ClearWhitelist() => execute_clear_whitelist(deps, env, info), + } + } + + #[entry_point] + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Governance() => cw_ics721_governance::query_governance(deps.storage), + QueryMsg::Whitelist {} => to_binary(&WHITELIST.query_whitelist(deps.storage)?), + QueryMsg::Whitelisted { value } => { + to_binary(&WHITELIST.query_is_whitelisted(deps.storage, &value)?) + } + } + } + + #[entry_point] + pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + match msg { + MigrateMsg::WithUpdate { + whitelist, + transfer_fee, + origin, + owner, + } => { + if let Some(list) = whitelist.clone() { + list.iter() + .map(|item| WHITELIST.add(deps.storage, &item.to_string())) + .collect::>>()?; + } + let res = cw_ics721_governance::migrate(deps, owner, origin, transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + whitelist.map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + } + } +} + +pub fn execute_add_to_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + value: &String, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.add(deps.storage, value)?; + Ok(Response::default() + .add_attribute("method", "execute_add_to_whitelist") + .add_attribute("value", value.to_string())) +} + +pub fn execute_remove_from_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + value: &String, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.remove(deps.storage, value)?; + Ok(Response::default() + .add_attribute("method", "execute_remove_from_whitelist") + .add_attribute("value", value.to_string())) +} + +pub fn execute_clear_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.clear(deps.storage)?; + Ok(Response::default().add_attribute("method", "execute_clear_whitelist")) +} + +pub fn execute_receive_nft( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: Cw721ReceiveMsg, +) -> Result { + let IbcOutgoingMsg { + channel_id, + memo: _, + receiver: _, + timeout: _, + } = from_binary(&msg.msg)?; + is_whitelisted(deps.storage, channel_id)?; + Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) +} + +pub fn execute_bridge_nft( + deps: DepsMut, + env: Env, + info: MessageInfo, + collection: String, + token_id: String, + msg: Binary, +) -> Result { + let IbcOutgoingMsg { + channel_id, + memo: _, + receiver: _, + timeout: _, + } = from_binary(&msg)?; + is_whitelisted(deps.storage, channel_id)?; + Ok(cw_ics721_governance::execute_bridge_nft( + deps, env, &info, collection, token_id, msg, + )?) +} + +pub fn is_whitelisted(storage: &dyn Storage, requestee: String) -> Result<(), ContractError> { + match WHITELIST.query_is_whitelisted(storage, &requestee)? { + true => Ok(()), + false => Err(ContractError::NotWhitelisted { requestee }), + } +} diff --git a/contracts/cw721-governed-channel-proxy/src/msg.rs b/contracts/cw721-governed-channel-proxy/src/msg.rs new file mode 100644 index 0000000..47e30ca --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/src/msg.rs @@ -0,0 +1,40 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Coin; +use cw_ics721_governance::{cw_ics721_governance_execute, cw_ics721_governance_query}; + +#[cw_serde] +pub struct InstantiateMsg { + pub origin: Option, + pub owner: Option, + pub transfer_fee: Option, + pub whitelist: Option>, +} + +#[cw_ics721_governance_execute] +#[cw_serde] +pub enum ExecuteMsg { + AddToWhitelist { value: String }, + RemoveFromWhitelist { value: String }, + ClearWhitelist(), +} + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Vec)] + Whitelist {}, + + #[returns(bool)] + Whitelisted { value: String }, +} + +#[cw_serde] +pub enum MigrateMsg { + WithUpdate { + origin: Option, + owner: Option, + transfer_fee: Option, + whitelist: Option>, + }, +} diff --git a/contracts/cw721-governed-channel-proxy/src/state.rs b/contracts/cw721-governed-channel-proxy/src/state.rs new file mode 100644 index 0000000..c01663d --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/src/state.rs @@ -0,0 +1,6 @@ +use cw721_whitelist::Whitelist; + +pub const CONTRACT_NAME: &str = "crates.io:cw721-governed-channel-proxy"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const WHITELIST: Whitelist = Whitelist::new(); diff --git a/contracts/cw721-governed-channel-proxy/src/tests.rs b/contracts/cw721-governed-channel-proxy/src/tests.rs new file mode 100644 index 0000000..cbcd84e --- /dev/null +++ b/contracts/cw721-governed-channel-proxy/src/tests.rs @@ -0,0 +1,393 @@ +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty, StdResult}; +use cw721_proxy_multi_test::Test as GovernedMultiTest; +use cw_ics721_governance::{Action, GovernanceError}; +use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; + +use crate::{ + entry, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +fn proxy_code() -> Box> { + let contract = ContractWrapper::new(entry::execute, entry::instantiate, entry::query); + Box::new(contract) +} + +pub struct Test { + pub governed_multi_test: GovernedMultiTest, + pub proxy_code_id: u64, + pub proxy: Addr, +} + +impl Test { + pub fn new( + cw721s: usize, + transfer_fee: Option, + set_owner: bool, + whitelist: Option>, + ) -> Self { + let mut governed_multi_test = GovernedMultiTest::new(cw721s, transfer_fee); + let proxy_code_id = governed_multi_test.app.store_code(proxy_code()); + let owner = match set_owner { + true => Some(governed_multi_test.minter.to_string()), + false => None, + }; + let proxy = governed_multi_test + .app + .instantiate_contract( + proxy_code_id, + governed_multi_test.minter.clone(), + &InstantiateMsg { + origin: Some(governed_multi_test.mock_receiver.to_string()), + owner, + transfer_fee: governed_multi_test.transfer_fee.clone(), + whitelist, + }, + &[], + "governed proxy", + None, + ) + .unwrap(); + Self { + governed_multi_test, + proxy_code_id, + proxy, + } + } + + pub fn add_to_whitelist( + &mut self, + owner: Addr, + channel: String, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::AddToWhitelist { value: channel }, + &[], + )?; + Ok(res) + } + + pub fn remove_from_whitelist( + &mut self, + owner: Addr, + channel: String, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::RemoveFromWhitelist { value: channel }, + &[], + )?; + Ok(res) + } + + pub fn clear_whitelist(&mut self, owner: Addr) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::ClearWhitelist(), + &[], + )?; + Ok(res) + } + + pub fn query_whitelist(&self) -> StdResult> { + // in case proxy passed message to origin + self.governed_multi_test + .app + .wrap() + .query_wasm_smart(&self.proxy, &QueryMsg::Whitelist {}) + } + + pub fn bridge_nft( + &mut self, + sender: Addr, + proxy: Addr, + collection: Addr, + token_id: String, + channel_id: String, + transfer_fee: Option, + ) -> Result { + let funds = transfer_fee.map_or(vec![], |fee| vec![fee]); + let res = self.governed_multi_test.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::BridgeNft { + collection: collection.to_string(), + token_id, + msg: to_binary(&self.governed_multi_test.ibc_outgoing_msg(channel_id))?, + }), + &funds, + )?; + + Ok(res) + } +} + +#[test] +fn add_to_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + assert_eq!(test.query_whitelist().unwrap(), Vec::::new()); + test.add_to_whitelist(test.governed_multi_test.minter.clone(), "any".to_string()) + .unwrap(); + assert_eq!(test.query_whitelist().unwrap(), vec!["any".to_string()]); +} + +#[test] +fn add_to_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let channel = "any"; + let err: ContractError = test + .add_to_whitelist(Addr::unchecked("unauthorized"), channel.to_string()) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn add_to_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let channel = "any"; + let err: ContractError = test + .add_to_whitelist(Addr::unchecked("unauthorized"), channel.to_string()) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn remove_from_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let whitelist = vec!["any".to_string()]; + let mut test = Test::new(1, transfer_fee, true, Some(whitelist.clone())); + assert_eq!(test.query_whitelist().unwrap(), whitelist,); + test.remove_from_whitelist(test.governed_multi_test.minter.clone(), "any".to_string()) + .unwrap(); + assert_eq!(test.query_whitelist().unwrap(), Vec::::new(),); +} + +#[test] +fn remove_from_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .remove_from_whitelist(Addr::unchecked("unauthorized"), "any".to_string()) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn remove_from_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .remove_from_whitelist(Addr::unchecked("unauthorized"), "any".to_string()) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn clear_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let whitelist = vec!["any".to_string()]; + let mut test = Test::new(1, transfer_fee, true, Some(whitelist.clone())); + assert_eq!(test.query_whitelist().unwrap(), whitelist,); + + test.clear_whitelist(test.governed_multi_test.minter.clone()) + .unwrap(); + assert_eq!(test.query_whitelist().unwrap(), Vec::::new(),) +} + +#[test] +fn clear_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn clear_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +//-- from governed test, test bridge and send nft again, due to new whitelist logic + +#[test] +fn bridge_nft_no_transfer_fee_whitelisted() { + let channel = "any"; + let mut test = Test::new(1, None, true, Some(vec![channel.to_string()])); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + test.bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary( + &test + .governed_multi_test + .ibc_outgoing_msg(channel.to_string()) + ) + .unwrap(), + } + ) + } + } +} + +#[test] +fn send_nft_no_transfer_fee_whitelisted() { + let channel = "any"; + let mut test = Test::new(1, None, true, Some(vec![channel.to_string()])); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + test.governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary(&test.governed_multi_test.ibc_outgoing_msg("any".to_string())) + .unwrap(), + } + ) + } + } +} + +#[test] +fn bridge_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: channel.to_string() + } + ) +} + +#[test] +fn send_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: channel.to_string() + } + ) +} +// ---- diff --git a/contracts/cw721-governed-code-id-proxy/.cargo/config b/contracts/cw721-governed-code-id-proxy/.cargo/config new file mode 100644 index 0000000..af5698e --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/cw721-governed-code-id-proxy/.gitignore b/contracts/cw721-governed-code-id-proxy/.gitignore new file mode 100644 index 0000000..dfdaaa6 --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/cw721-governed-code-id-proxy/Cargo.toml b/contracts/cw721-governed-code-id-proxy/Cargo.toml new file mode 100644 index 0000000..8d3b3d9 --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "cw721-governed_code-id-proxy" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-proxy = { workspace = true } +cw721-proxy-derive = { workspace = true } +cw721-whitelist = { workspace = true } +cw721-governed-proxy = { workspace = true, features = ["library"] } +cw-ics721-governance = { workspace = true} +ibc-outgoing-msg = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-base = { workspace = true } +cw721-proxy-tester = { workspace = true } +cw721-proxy-multi-test = { workspace = true } +rand = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/cw721-governed-code-id-proxy/schema/cw721-governed_code-id-proxy.json b/contracts/cw721-governed-code-id-proxy/schema/cw721-governed_code-id-proxy.json new file mode 100644 index 0000000..dee9bed --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/schema/cw721-governed_code-id-proxy.json @@ -0,0 +1,449 @@ +{ + "contract_name": "cw721-governed_code-id-proxy", + "contract_version": "0.0.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "properties": { + "origin": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "whitelist": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "add_to_whitelist" + ], + "properties": { + "add_to_whitelist": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_from_whitelist" + ], + "properties": { + "remove_from_whitelist": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "clear_whitelist" + ], + "properties": { + "clear_whitelist": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "description": "Actions that can be taken to alter the proxy contract's governance like in `execute`'s entry point: ```rust use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; use cosmwasm_schema::cw_serde; use cosmwasm_std::entry_point;\n\n#[cw_serde] pub enum ExecuteMsg { Governance(cw_ics721_governance::Action), ReceiveNft(cw721::Cw721ReceiveMsg) }\n\n#[cfg_attr(not(feature = \"library\"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::Governance(action) => { Ok(cw_ics721_governance::execute(deps, env, &info, action)?) } ExecuteMsg::ReceiveNft(msg) => { Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) } } } ```", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Cw721ReceiveMsg to be forwared to ICS721 (origin). NOTE: this is NOT part of governance, since it is send by cw721 contract diretly to proxy", + "type": "object", + "required": [ + "receive_nft" + ], + "properties": { + "receive_nft": { + "$ref": "#/definitions/Cw721ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the proxy contract's governance", + "oneOf": [ + { + "description": "Changing owner of proxy. Once set, it can't be set to None - except via migration.", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "ICS721 contract where Cw721ReceiveMsg is forwarded to.", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds!", + "type": "object", + "required": [ + "transfer_fee" + ], + "properties": { + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "description": "Send funds from proxy to specified address.", + "type": "object", + "required": [ + "send_funds" + ], + "properties": { + "send_funds": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721.", + "type": "object", + "required": [ + "bridge_nft" + ], + "properties": { + "bridge_nft": { + "type": "object", + "required": [ + "collection", + "msg", + "token_id" + ], + "properties": { + "collection": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Cw721ReceiveMsg": { + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "msg", + "sender", + "token_id" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "whitelist" + ], + "properties": { + "whitelist": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's governance information", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "governance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Governance", + "description": "A governed contract may have: - an optional owner, - an origin (ICS721) where msgs are forwarded to, and - an optional transfer fee.\n\nOwner: - used in assert_owner(). For example execute_transfer_fee() allows only owner to change fees.\n\nOrigin: - ...\n\nTransfer Fee: - ...", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "$ref": "#/definitions/Addr" + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "whitelist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_uint64", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "whitelisted": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + } + } +} diff --git a/contracts/cw721-governed-code-id-proxy/src/bin/schema.rs b/contracts/cw721-governed-code-id-proxy/src/bin/schema.rs new file mode 100644 index 0000000..5b5b959 --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw721_governed_code_id_proxy::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/cw721-governed-code-id-proxy/src/error.rs b/contracts/cw721-governed-code-id-proxy/src/error.rs new file mode 100644 index 0000000..58fc7e4 --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/src/error.rs @@ -0,0 +1,15 @@ +use cosmwasm_std::StdError; +use cw_ics721_governance::GovernanceError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Governance(#[from] GovernanceError), + + #[error("{requestee} not whitelisted!")] + NotWhitelisted { requestee: u64 }, +} diff --git a/contracts/cw721-governed-code-id-proxy/src/lib.rs b/contracts/cw721-governed-code-id-proxy/src/lib.rs new file mode 100644 index 0000000..eabcf68 --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/src/lib.rs @@ -0,0 +1,178 @@ +use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo, Response, Storage}; +use cw721::Cw721ReceiveMsg; +use error::ContractError; +use state::WHITELIST; + +pub mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +#[cfg(not(feature = "library"))] +pub mod entry { + use crate::error::ContractError; + use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + use crate::state::{CONTRACT_NAME, CONTRACT_VERSION, WHITELIST}; + use crate::{ + execute_add_to_whitelist, execute_bridge_nft, execute_clear_whitelist, execute_receive_nft, + execute_remove_from_whitelist, + }; + + use cosmwasm_std::{entry_point, to_binary}; + use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + use cw2::set_contract_version; + use cw_ics721_governance::Action; + + // This makes a conscious choice on the various generics used by the contract + #[entry_point] + pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + WHITELIST.init(deps.storage, &msg.whitelist)?; + let res = + cw_ics721_governance::instantiate(deps, info, msg.owner, msg.origin, msg.transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + msg.whitelist + .map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + + #[entry_point] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + match msg { + ExecuteMsg::Governance(Action::BridgeNft { + collection, + token_id, + msg, + }) => execute_bridge_nft(deps, env, info, collection, token_id, msg), + ExecuteMsg::Governance(action) => { + Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + } + ExecuteMsg::ReceiveNft(msg) => execute_receive_nft(deps, env, info, msg), + ExecuteMsg::AddToWhitelist { value } => { + execute_add_to_whitelist(deps, env, info, &value) + } + ExecuteMsg::RemoveFromWhitelist { value } => { + execute_remove_from_whitelist(deps, env, info, &value) + } + ExecuteMsg::ClearWhitelist() => execute_clear_whitelist(deps, env, info), + } + } + + #[entry_point] + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Governance() => cw_ics721_governance::query_governance(deps.storage), + QueryMsg::Whitelist {} => to_binary(&WHITELIST.query_whitelist(deps.storage)?), + QueryMsg::Whitelisted { value } => { + to_binary(&WHITELIST.query_is_whitelisted(deps.storage, &value)?) + } + } + } + + #[entry_point] + pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + match msg { + MigrateMsg::WithUpdate { + origin, + owner, + transfer_fee, + whitelist, + } => { + if let Some(list) = whitelist.clone() { + list.iter() + .map(|item| WHITELIST.add(deps.storage, item)) + .collect::>>()?; + } + let res = cw_ics721_governance::migrate(deps, owner, origin, transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + whitelist.map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + } + } +} + +pub fn execute_add_to_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + value: &u64, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.add(deps.storage, value)?; + Ok(Response::default() + .add_attribute("method", "execute_add_to_whitelist") + .add_attribute("value", value.to_string())) +} + +pub fn execute_remove_from_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + value: &u64, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.remove(deps.storage, value)?; + Ok(Response::default() + .add_attribute("method", "execute_remove_from_whitelist") + .add_attribute("value", value.to_string())) +} + +pub fn execute_clear_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.clear(deps.storage)?; + Ok(Response::default().add_attribute("method", "execute_clear_whitelist")) +} + +pub fn execute_receive_nft( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: Cw721ReceiveMsg, +) -> Result { + let contract_info = deps.querier.query_wasm_contract_info(info.sender.clone())?; + is_whitelisted(deps.storage, contract_info.code_id)?; + Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) +} + +pub fn execute_bridge_nft( + deps: DepsMut, + env: Env, + info: MessageInfo, + collection: String, + token_id: String, + msg: Binary, +) -> Result { + let contract_info = deps.querier.query_wasm_contract_info(collection.clone())?; + is_whitelisted(deps.storage, contract_info.code_id)?; + Ok(cw_ics721_governance::execute_bridge_nft( + deps, env, &info, collection, token_id, msg, + )?) +} + +pub fn is_whitelisted(storage: &dyn Storage, requestee: u64) -> Result<(), ContractError> { + match WHITELIST.query_is_whitelisted(storage, &requestee)? { + true => Ok(()), + false => Err(ContractError::NotWhitelisted { requestee }), + } +} diff --git a/contracts/cw721-governed-code-id-proxy/src/msg.rs b/contracts/cw721-governed-code-id-proxy/src/msg.rs new file mode 100644 index 0000000..53d5462 --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/src/msg.rs @@ -0,0 +1,40 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Coin; +use cw_ics721_governance::{cw_ics721_governance_execute, cw_ics721_governance_query}; + +#[cw_serde] +pub struct InstantiateMsg { + pub origin: Option, + pub owner: Option, + pub transfer_fee: Option, + pub whitelist: Option>, +} + +#[cw_ics721_governance_execute] +#[cw_serde] +pub enum ExecuteMsg { + AddToWhitelist { value: u64 }, + RemoveFromWhitelist { value: u64 }, + ClearWhitelist(), +} + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Vec)] + Whitelist {}, + + #[returns(bool)] + Whitelisted { value: u64 }, +} + +#[cw_serde] +pub enum MigrateMsg { + WithUpdate { + origin: Option, + owner: Option, + transfer_fee: Option, + whitelist: Option>, + }, +} diff --git a/contracts/cw721-governed-code-id-proxy/src/state.rs b/contracts/cw721-governed-code-id-proxy/src/state.rs new file mode 100644 index 0000000..fda580c --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/src/state.rs @@ -0,0 +1,6 @@ +use cw721_whitelist::Whitelist; + +pub const CONTRACT_NAME: &str = "crates.io:cw721-governed_code-id-proxy"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const WHITELIST: Whitelist = Whitelist::new(); diff --git a/contracts/cw721-governed-code-id-proxy/src/tests.rs b/contracts/cw721-governed-code-id-proxy/src/tests.rs new file mode 100644 index 0000000..b7a26dd --- /dev/null +++ b/contracts/cw721-governed-code-id-proxy/src/tests.rs @@ -0,0 +1,396 @@ +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty, StdResult}; +use cw721_proxy_multi_test::Test as GovernedMultiTest; +use cw_ics721_governance::{Action, GovernanceError}; +use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; + +use crate::{ + entry, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +fn proxy_code() -> Box> { + let contract = ContractWrapper::new(entry::execute, entry::instantiate, entry::query); + Box::new(contract) +} + +pub struct Test { + pub governed_multi_test: GovernedMultiTest, + pub proxy_code_id: u64, + pub proxy: Addr, +} + +impl Test { + pub fn new( + cw721s: usize, + transfer_fee: Option, + set_owner: bool, + whitelist: Option>, + ) -> Self { + let mut governed_multi_test = GovernedMultiTest::new(cw721s, transfer_fee); + let proxy_code_id = governed_multi_test.app.store_code(proxy_code()); + let owner = match set_owner { + true => Some(governed_multi_test.minter.to_string()), + false => None, + }; + let proxy = governed_multi_test + .app + .instantiate_contract( + proxy_code_id, + governed_multi_test.minter.clone(), + &InstantiateMsg { + origin: Some(governed_multi_test.mock_receiver.to_string()), + owner, + transfer_fee: governed_multi_test.transfer_fee.clone(), + whitelist, + }, + &[], + "governed proxy", + None, + ) + .unwrap(); + Self { + governed_multi_test, + proxy_code_id, + proxy, + } + } + + pub fn add_to_whitelist( + &mut self, + owner: Addr, + value: u64, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::AddToWhitelist { value }, + &[], + )?; + Ok(res) + } + + pub fn remove_from_whitelist( + &mut self, + owner: Addr, + value: u64, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::RemoveFromWhitelist { value }, + &[], + )?; + Ok(res) + } + + pub fn clear_whitelist(&mut self, owner: Addr) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::ClearWhitelist(), + &[], + )?; + Ok(res) + } + + pub fn query_whitelist(&self) -> StdResult> { + // in case proxy passed message to origin + self.governed_multi_test + .app + .wrap() + .query_wasm_smart(&self.proxy, &QueryMsg::Whitelist {}) + } + + pub fn bridge_nft( + &mut self, + sender: Addr, + proxy: Addr, + collection: Addr, + token_id: String, + channel_id: String, + transfer_fee: Option, + ) -> Result { + let funds = transfer_fee.map_or(vec![], |fee| vec![fee]); + let res = self.governed_multi_test.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::BridgeNft { + collection: collection.to_string(), + token_id, + msg: to_binary(&self.governed_multi_test.ibc_outgoing_msg(channel_id))?, + }), + &funds, + )?; + + Ok(res) + } +} + +#[test] +fn add_to_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + test.add_to_whitelist(test.governed_multi_test.minter.clone(), 1234) + .unwrap(); +} + +#[test] +fn add_to_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .add_to_whitelist(Addr::unchecked("unauthorized"), 1234) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn add_to_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .add_to_whitelist(Addr::unchecked("unauthorized"), 1234) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn remove_from_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + test.remove_from_whitelist(test.governed_multi_test.minter.clone(), 1234) + .unwrap(); +} + +#[test] +fn remove_from_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .remove_from_whitelist(Addr::unchecked("unauthorized"), 1234) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn remove_from_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .remove_from_whitelist(Addr::unchecked("unauthorized"), 1234) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn clear_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let whitelist = vec![1234]; + let mut test = Test::new(1, transfer_fee, true, Some(whitelist.clone())); + assert_eq!(test.query_whitelist().unwrap(), whitelist,); + + test.clear_whitelist(test.governed_multi_test.minter.clone()) + .unwrap(); + assert_eq!(test.query_whitelist().unwrap(), Vec::::new(),) +} + +#[test] +fn clear_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn clear_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +//-- from governed test, test bridge and send nft again, due to new whitelist logic + +#[test] +fn bridge_nft_no_transfer_fee_whitelisted() { + let mut test = Test::new(1, None, true, None); + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721_id, + ) + .unwrap(); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + test.bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary( + &test + .governed_multi_test + .ibc_outgoing_msg(channel.to_string()) + ) + .unwrap(), + } + ) + } + } +} + +#[test] +fn send_nft_no_transfer_fee_whitelisted() { + let mut test = Test::new(1, None, true, None); + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721_id, + ) + .unwrap(); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + test.governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary(&test.governed_multi_test.ibc_outgoing_msg("any".to_string())) + .unwrap(), + } + ) + } + } +} + +#[test] +fn bridge_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: test.governed_multi_test.cw721_id + } + ) +} + +#[test] +fn send_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: test.governed_multi_test.cw721_id + } + ) +} +// ---- diff --git a/contracts/cw721-governed-collection-channels-proxy/.cargo/config b/contracts/cw721-governed-collection-channels-proxy/.cargo/config new file mode 100644 index 0000000..af5698e --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/cw721-governed-collection-channels-proxy/Cargo.toml b/contracts/cw721-governed-collection-channels-proxy/Cargo.toml new file mode 100644 index 0000000..47eefd7 --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "cw721-governed-collection-channels-proxy" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-proxy = { workspace = true } +cw721-proxy-derive = { workspace = true } +cw721-whitelist-map = { workspace = true } +cw721-governed-proxy = { workspace = true, features = ["library"] } +cw-ics721-governance = { workspace = true} +ibc-outgoing-msg = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-base = { workspace = true } +cw721-proxy-tester = { workspace = true } +cw721-proxy-multi-test = { workspace = true } +rand = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/cw721-governed-collection-channels-proxy/schema/cw721-governed-collection-channels-proxy.json b/contracts/cw721-governed-collection-channels-proxy/schema/cw721-governed-collection-channels-proxy.json new file mode 100644 index 0000000..176ab96 --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/schema/cw721-governed-collection-channels-proxy.json @@ -0,0 +1,478 @@ +{ + "contract_name": "cw721-governed-collection-channels-proxy", + "contract_version": "0.0.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "properties": { + "origin": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "whitelist": { + "type": [ + "array", + "null" + ], + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "add_to_whitelist" + ], + "properties": { + "add_to_whitelist": { + "type": "object", + "required": [ + "channels", + "collection" + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "string" + } + }, + "collection": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_from_whitelist" + ], + "properties": { + "remove_from_whitelist": { + "type": "object", + "required": [ + "collection" + ], + "properties": { + "collection": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "clear_whitelist" + ], + "properties": { + "clear_whitelist": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "description": "Actions that can be taken to alter the proxy contract's governance like in `execute`'s entry point: ```rust use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; use cosmwasm_schema::cw_serde; use cosmwasm_std::entry_point;\n\n#[cw_serde] pub enum ExecuteMsg { Governance(cw_ics721_governance::Action), ReceiveNft(cw721::Cw721ReceiveMsg) }\n\n#[cfg_attr(not(feature = \"library\"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::Governance(action) => { Ok(cw_ics721_governance::execute(deps, env, &info, action)?) } ExecuteMsg::ReceiveNft(msg) => { Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) } } } ```", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Cw721ReceiveMsg to be forwared to ICS721 (origin). NOTE: this is NOT part of governance, since it is send by cw721 contract diretly to proxy", + "type": "object", + "required": [ + "receive_nft" + ], + "properties": { + "receive_nft": { + "$ref": "#/definitions/Cw721ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the proxy contract's governance", + "oneOf": [ + { + "description": "Changing owner of proxy. Once set, it can't be set to None - except via migration.", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "ICS721 contract where Cw721ReceiveMsg is forwarded to.", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds!", + "type": "object", + "required": [ + "transfer_fee" + ], + "properties": { + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "description": "Send funds from proxy to specified address.", + "type": "object", + "required": [ + "send_funds" + ], + "properties": { + "send_funds": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721.", + "type": "object", + "required": [ + "bridge_nft" + ], + "properties": { + "bridge_nft": { + "type": "object", + "required": [ + "collection", + "msg", + "token_id" + ], + "properties": { + "collection": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Cw721ReceiveMsg": { + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "msg", + "sender", + "token_id" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Gets a list of collection and channels authorized for ICS721 transfers.", + "type": "object", + "required": [ + "whitelist" + ], + "properties": { + "whitelist": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "True in case CW721 contract and channel is authorized for ICS721 transfers.", + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "type": "object", + "required": [ + "channel", + "collection" + ], + "properties": { + "channel": { + "type": "string" + }, + "collection": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's governance information", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "governance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Governance", + "description": "A governed contract may have: - an optional owner, - an origin (ICS721) where msgs are forwarded to, and - an optional transfer fee.\n\nOwner: - used in assert_owner(). For example execute_transfer_fee() allows only owner to change fees.\n\nOrigin: - ...\n\nTransfer Fee: - ...", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "$ref": "#/definitions/Addr" + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "whitelist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Tuple_of_String_and_Array_of_String", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "whitelisted": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + } + } +} diff --git a/contracts/cw721-governed-collection-channels-proxy/src/bin/schema.rs b/contracts/cw721-governed-collection-channels-proxy/src/bin/schema.rs new file mode 100644 index 0000000..ce0b919 --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw721_governed_collection_channels_proxy::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/cw721-governed-collection-channels-proxy/src/error.rs b/contracts/cw721-governed-collection-channels-proxy/src/error.rs new file mode 100644 index 0000000..987847a --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/src/error.rs @@ -0,0 +1,15 @@ +use cosmwasm_std::StdError; +use cw_ics721_governance::GovernanceError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Governance(#[from] GovernanceError), + + #[error("{requestee} not whitelisted!")] + NotWhitelisted { requestee: String }, +} diff --git a/contracts/cw721-governed-collection-channels-proxy/src/lib.rs b/contracts/cw721-governed-collection-channels-proxy/src/lib.rs new file mode 100644 index 0000000..3847309 --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/src/lib.rs @@ -0,0 +1,221 @@ +use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; +use cw721::Cw721ReceiveMsg; +use error::ContractError; +use ibc_outgoing_msg::IbcOutgoingMsg; +use state::WHITELIST; + +pub mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +#[cfg(not(feature = "library"))] +pub mod entry { + use crate::error::ContractError; + use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + use crate::state::{CONTRACT_NAME, CONTRACT_VERSION, WHITELIST}; + use crate::{ + execute_add_to_whitelist, execute_bridge_nft, execute_clear_whitelist, execute_receive_nft, + execute_remove_from_whitelist, + }; + + use cosmwasm_std::{entry_point, to_binary, Order}; + use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + use cw2::set_contract_version; + use cw_ics721_governance::Action; + + // This makes a conscious choice on the various generics used by the contract + #[entry_point] + pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + if let Some(list) = msg.whitelist.clone() { + list.iter() + .map(|item| { + deps.api.addr_validate(item.0.as_str())?; + WHITELIST.save(deps.storage, item.0.to_string(), &item.1) + }) + .collect::>>()?; + } + let res = + cw_ics721_governance::instantiate(deps, info, msg.owner, msg.origin, msg.transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + msg.whitelist + .map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + + #[entry_point] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + match msg { + ExecuteMsg::Governance(Action::BridgeNft { + collection, + token_id, + msg, + }) => execute_bridge_nft(deps, env, info, collection, token_id, msg), + ExecuteMsg::Governance(action) => { + Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + } + ExecuteMsg::ReceiveNft(msg) => execute_receive_nft(deps, env, info, msg), + ExecuteMsg::AddToWhitelist { + collection, + channels, + } => execute_add_to_whitelist(deps, env, info, collection, channels), + ExecuteMsg::RemoveFromWhitelist { collection } => { + execute_remove_from_whitelist(deps, env, info, collection) + } + ExecuteMsg::ClearWhitelist() => execute_clear_whitelist(deps, env, info), + } + } + + #[entry_point] + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Governance() => cw_ics721_governance::query_governance(deps.storage), + QueryMsg::Whitelist {} => to_binary( + &WHITELIST + .map + .range(deps.storage, None, None, Order::Ascending) + .collect::)>>>()?, + ), + QueryMsg::Whitelisted { + collection, + channel, + } => to_binary(&WHITELIST.query_is_whitelisted( + deps.storage, + collection, + |channels| channels.contains(&channel), + )?), + } + } + + #[entry_point] + pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + match msg { + MigrateMsg::WithUpdate { + origin, + owner, + transfer_fee, + whitelist, + } => { + if let Some(list) = whitelist.clone() { + list.iter() + .map(|item| WHITELIST.save(deps.storage, item.0.clone(), &item.1)) + .collect::>>()?; + } + let res = cw_ics721_governance::migrate(deps, owner, origin, transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + whitelist.map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + } + } +} + +pub fn execute_add_to_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + collection: String, + channels: Vec, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.save(deps.storage, collection.clone(), &channels)?; + Ok(Response::default() + .add_attribute("method", "execute_add_to_whitelist") + .add_attribute("collection", collection) + .add_attribute("channels", format!("{:?}", channels))) +} + +pub fn execute_remove_from_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + collection: String, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.remove(deps.storage, collection.clone()); + Ok(Response::default() + .add_attribute("method", "execute_remove_from_whitelist") + .add_attribute("key", collection)) +} + +pub fn execute_clear_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.clear(deps.storage)?; + Ok(Response::default().add_attribute("method", "execute_clear_whitelist")) +} + +pub fn execute_receive_nft( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: Cw721ReceiveMsg, +) -> Result { + let IbcOutgoingMsg { + channel_id, + memo: _, + receiver: _, + timeout: _, + } = from_binary(&msg.msg)?; + is_whitelisted(deps.storage, info.sender.to_string(), channel_id)?; + Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) +} + +pub fn execute_bridge_nft( + deps: DepsMut, + env: Env, + info: MessageInfo, + collection: String, + token_id: String, + msg: Binary, +) -> Result { + let IbcOutgoingMsg { + channel_id, + memo: _, + receiver: _, + timeout: _, + } = from_binary(&msg)?; + is_whitelisted(deps.storage, collection.clone(), channel_id)?; + Ok(cw_ics721_governance::execute_bridge_nft( + deps, env, &info, collection, token_id, msg, + )?) +} + +pub fn is_whitelisted( + storage: &dyn Storage, + collection: String, + channel: String, +) -> Result<(), ContractError> { + if !WHITELIST.has(storage, collection.clone()) { + Err(ContractError::NotWhitelisted { + requestee: collection, + }) + } else { + match WHITELIST + .query_is_whitelisted(storage, collection, |channels| channels.contains(&channel))? + { + true => Ok(()), + false => Err(ContractError::NotWhitelisted { requestee: channel }), + } + } +} diff --git a/contracts/cw721-governed-collection-channels-proxy/src/msg.rs b/contracts/cw721-governed-collection-channels-proxy/src/msg.rs new file mode 100644 index 0000000..d9c97bc --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/src/msg.rs @@ -0,0 +1,47 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Coin; +use cw_ics721_governance::{cw_ics721_governance_execute, cw_ics721_governance_query}; + +#[cw_serde] +pub struct InstantiateMsg { + pub origin: Option, + pub owner: Option, + pub transfer_fee: Option, + pub whitelist: Option)>>, +} + +#[cw_ics721_governance_execute] +#[cw_serde] +pub enum ExecuteMsg { + AddToWhitelist { + collection: String, + channels: Vec, + }, + RemoveFromWhitelist { + collection: String, + }, + ClearWhitelist(), +} + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Gets a list of collection and channels authorized for ICS721 transfers. + #[returns(Vec<(String, Vec)>)] + Whitelist {}, + + /// True in case CW721 contract and channel is authorized for ICS721 transfers. + #[returns(bool)] + Whitelisted { collection: String, channel: String }, +} + +#[cw_serde] +pub enum MigrateMsg { + WithUpdate { + origin: Option, + owner: Option, + transfer_fee: Option, + whitelist: Option)>>, + }, +} diff --git a/contracts/cw721-governed-collection-channels-proxy/src/state.rs b/contracts/cw721-governed-collection-channels-proxy/src/state.rs new file mode 100644 index 0000000..f222f54 --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/src/state.rs @@ -0,0 +1,6 @@ +use cw721_whitelist_map::WhiteListMap; + +pub const CONTRACT_NAME: &str = "crates.io:cw721-governed-collection-channels-proxy"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const WHITELIST: WhiteListMap> = WhiteListMap::new(); diff --git a/contracts/cw721-governed-collection-channels-proxy/src/tests.rs b/contracts/cw721-governed-collection-channels-proxy/src/tests.rs new file mode 100644 index 0000000..55de2c8 --- /dev/null +++ b/contracts/cw721-governed-collection-channels-proxy/src/tests.rs @@ -0,0 +1,436 @@ +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty, StdResult}; +use cw721_proxy_multi_test::Test as GovernedMultiTest; +use cw_ics721_governance::{Action, GovernanceError}; +use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; + +use crate::{ + entry, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +fn proxy_code() -> Box> { + let contract = ContractWrapper::new(entry::execute, entry::instantiate, entry::query); + Box::new(contract) +} + +pub struct Test { + pub governed_multi_test: GovernedMultiTest, + pub proxy_code_id: u64, + pub proxy: Addr, +} + +impl Test { + pub fn new( + cw721s: usize, + transfer_fee: Option, + set_owner: bool, + whitelist: Option)>>, + ) -> Self { + let mut governed_multi_test = GovernedMultiTest::new(cw721s, transfer_fee); + let proxy_code_id = governed_multi_test.app.store_code(proxy_code()); + let owner = match set_owner { + true => Some(governed_multi_test.minter.to_string()), + false => None, + }; + let proxy = governed_multi_test + .app + .instantiate_contract( + proxy_code_id, + governed_multi_test.minter.clone(), + &InstantiateMsg { + origin: Some(governed_multi_test.mock_receiver.to_string()), + owner, + transfer_fee: governed_multi_test.transfer_fee.clone(), + whitelist, + }, + &[], + "governed proxy", + None, + ) + .unwrap(); + Self { + governed_multi_test, + proxy_code_id, + proxy, + } + } + + pub fn add_to_whitelist( + &mut self, + owner: Addr, + collection: String, + channels: Vec, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::AddToWhitelist { + collection, + channels, + }, + &[], + )?; + Ok(res) + } + + pub fn remove_from_whitelist( + &mut self, + owner: Addr, + collection: String, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::RemoveFromWhitelist { collection }, + &[], + )?; + Ok(res) + } + + pub fn clear_whitelist(&mut self, owner: Addr) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::ClearWhitelist(), + &[], + )?; + Ok(res) + } + + pub fn query_whitelist(&self) -> StdResult)>> { + // in case proxy passed message to origin + self.governed_multi_test + .app + .wrap() + .query_wasm_smart(&self.proxy, &QueryMsg::Whitelist {}) + } + + pub fn bridge_nft( + &mut self, + sender: Addr, + proxy: Addr, + collection: Addr, + token_id: String, + channel_id: String, + transfer_fee: Option, + ) -> Result { + let funds = transfer_fee.map_or(vec![], |fee| vec![fee]); + let res = self.governed_multi_test.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::BridgeNft { + collection: collection.to_string(), + token_id, + msg: to_binary(&self.governed_multi_test.ibc_outgoing_msg(channel_id))?, + }), + &funds, + )?; + + Ok(res) + } +} + +#[test] +fn add_to_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + assert_eq!( + test.query_whitelist().unwrap(), + Vec::<(String, Vec)>::new() + ); + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721s[0].to_string(), + vec!["any".to_string()], + ) + .unwrap(); + assert_eq!( + test.query_whitelist().unwrap(), + vec![( + test.governed_multi_test.cw721s[0].to_string(), + vec!["any".to_string()] + )] + ); +} + +#[test] +fn add_to_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let channel = "any"; + let err: ContractError = test + .add_to_whitelist( + Addr::unchecked("unauthorized"), + test.governed_multi_test.cw721s[0].to_string(), + vec![channel.to_string()], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn add_to_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let channel = "any"; + let err: ContractError = test + .add_to_whitelist( + Addr::unchecked("unauthorized"), + test.governed_multi_test.cw721s[0].to_string(), + vec![channel.to_string()], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn remove_from_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let whitelist = ("foo".to_string(), vec!["any".to_string()]); + let mut test = Test::new(1, transfer_fee, true, Some(vec![whitelist.clone()])); + assert_eq!(test.query_whitelist().unwrap(), vec![whitelist],); + test.remove_from_whitelist(test.governed_multi_test.minter.clone(), "foo".to_string()) + .unwrap(); + assert_eq!( + test.query_whitelist().unwrap(), + Vec::<(String, Vec)>::new(), + ); +} + +#[test] +fn remove_from_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .remove_from_whitelist(Addr::unchecked("unauthorized"), "any".to_string()) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn remove_from_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .remove_from_whitelist(Addr::unchecked("unauthorized"), "any".to_string()) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn clear_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let whitelist = ("foo".to_string(), vec!["any".to_string()]); + let mut test = Test::new(1, transfer_fee, true, Some(vec![whitelist.clone()])); + assert_eq!(test.query_whitelist().unwrap(), vec![whitelist],); + + test.clear_whitelist(test.governed_multi_test.minter.clone()) + .unwrap(); + assert_eq!( + test.query_whitelist().unwrap(), + Vec::<(String, Vec)>::new(), + ) +} + +#[test] +fn clear_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn clear_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +//-- from governed test, test bridge and send nft again, due to new whitelist logic + +#[test] +fn bridge_nft_no_transfer_fee_whitelisted() { + let mut test = Test::new(1, None, true, None); + let channel = "any"; + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721s[0].to_string(), + vec![channel.to_string()], + ) + .unwrap(); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + test.bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary( + &test + .governed_multi_test + .ibc_outgoing_msg(channel.to_string()) + ) + .unwrap(), + } + ) + } + } +} + +#[test] +fn send_nft_no_transfer_fee_whitelisted() { + let mut test = Test::new(1, None, true, None); + let channel = "any"; + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721s[0].to_string(), + vec![channel.to_string()], + ) + .unwrap(); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + test.governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary(&test.governed_multi_test.ibc_outgoing_msg("any".to_string())) + .unwrap(), + } + ) + } + } +} + +#[test] +fn bridge_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: test.governed_multi_test.cw721s[0].to_string() + } + ) +} + +#[test] +fn send_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: test.governed_multi_test.cw721s[0].to_string() + } + ) +} +// ---- diff --git a/contracts/cw721-governed-collection-proxy/.cargo/config b/contracts/cw721-governed-collection-proxy/.cargo/config new file mode 100644 index 0000000..af5698e --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/cw721-governed-collection-proxy/.gitignore b/contracts/cw721-governed-collection-proxy/.gitignore new file mode 100644 index 0000000..dfdaaa6 --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/cw721-governed-collection-proxy/Cargo.toml b/contracts/cw721-governed-collection-proxy/Cargo.toml new file mode 100644 index 0000000..abdc7e6 --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "cw721-governed-collection-proxy" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-proxy = { workspace = true } +cw721-proxy-derive = { workspace = true } +cw721-whitelist = { workspace = true } +cw721-governed-proxy = { workspace = true, features = ["library"] } +cw-ics721-governance = { workspace = true} +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-base = { workspace = true } +cw721-proxy-tester = { workspace = true } +cw721-proxy-multi-test = { workspace = true } +rand = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/cw721-governed-collection-proxy/schema/cw721-governed-collection-proxy.json b/contracts/cw721-governed-collection-proxy/schema/cw721-governed-collection-proxy.json new file mode 100644 index 0000000..fac9c36 --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/schema/cw721-governed-collection-proxy.json @@ -0,0 +1,439 @@ +{ + "contract_name": "cw721-governed-collection-proxy", + "contract_version": "0.0.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "properties": { + "origin": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "whitelist": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "add_to_whitelist" + ], + "properties": { + "add_to_whitelist": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_from_whitelist" + ], + "properties": { + "remove_from_whitelist": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "clear_whitelist" + ], + "properties": { + "clear_whitelist": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + }, + { + "description": "Actions that can be taken to alter the proxy contract's governance like in `execute`'s entry point: ```rust use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; use cosmwasm_schema::cw_serde; use cosmwasm_std::entry_point;\n\n#[cw_serde] pub enum ExecuteMsg { Governance(cw_ics721_governance::Action), ReceiveNft(cw721::Cw721ReceiveMsg) }\n\n#[cfg_attr(not(feature = \"library\"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::Governance(action) => { Ok(cw_ics721_governance::execute(deps, env, &info, action)?) } ExecuteMsg::ReceiveNft(msg) => { Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) } } } ```", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Cw721ReceiveMsg to be forwared to ICS721 (origin). NOTE: this is NOT part of governance, since it is send by cw721 contract diretly to proxy", + "type": "object", + "required": [ + "receive_nft" + ], + "properties": { + "receive_nft": { + "$ref": "#/definitions/Cw721ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the proxy contract's governance", + "oneOf": [ + { + "description": "Changing owner of proxy. Once set, it can't be set to None - except via migration.", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "ICS721 contract where Cw721ReceiveMsg is forwarded to.", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds!", + "type": "object", + "required": [ + "transfer_fee" + ], + "properties": { + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "description": "Send funds from proxy to specified address.", + "type": "object", + "required": [ + "send_funds" + ], + "properties": { + "send_funds": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721.", + "type": "object", + "required": [ + "bridge_nft" + ], + "properties": { + "bridge_nft": { + "type": "object", + "required": [ + "collection", + "msg", + "token_id" + ], + "properties": { + "collection": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Cw721ReceiveMsg": { + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "msg", + "sender", + "token_id" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "whitelist" + ], + "properties": { + "whitelist": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's governance information", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "governance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Governance", + "description": "A governed contract may have: - an optional owner, - an origin (ICS721) where msgs are forwarded to, and - an optional transfer fee.\n\nOwner: - used in assert_owner(). For example execute_transfer_fee() allows only owner to change fees.\n\nOrigin: - ...\n\nTransfer Fee: - ...", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "$ref": "#/definitions/Addr" + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "whitelist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + }, + "whitelisted": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + } + } +} diff --git a/contracts/cw721-governed-collection-proxy/src/bin/schema.rs b/contracts/cw721-governed-collection-proxy/src/bin/schema.rs new file mode 100644 index 0000000..63a81e6 --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw721_governed_collection_proxy::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/cw721-governed-collection-proxy/src/error.rs b/contracts/cw721-governed-collection-proxy/src/error.rs new file mode 100644 index 0000000..987847a --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/src/error.rs @@ -0,0 +1,15 @@ +use cosmwasm_std::StdError; +use cw_ics721_governance::GovernanceError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Governance(#[from] GovernanceError), + + #[error("{requestee} not whitelisted!")] + NotWhitelisted { requestee: String }, +} diff --git a/contracts/cw721-governed-collection-proxy/src/lib.rs b/contracts/cw721-governed-collection-proxy/src/lib.rs new file mode 100644 index 0000000..67774ac --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/src/lib.rs @@ -0,0 +1,176 @@ +use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo, Response, Storage}; +use cw721::Cw721ReceiveMsg; +use error::ContractError; +use state::WHITELIST; + +pub mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +#[cfg(not(feature = "library"))] +pub mod entry { + use crate::error::ContractError; + use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + use crate::state::{CONTRACT_NAME, CONTRACT_VERSION, WHITELIST}; + use crate::{ + execute_add_to_whitelist, execute_bridge_nft, execute_clear_whitelist, execute_receive_nft, + execute_remove_from_whitelist, + }; + + use cosmwasm_std::{entry_point, to_binary}; + use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + use cw2::set_contract_version; + use cw_ics721_governance::Action; + + // This makes a conscious choice on the various generics used by the contract + #[entry_point] + pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + WHITELIST.init(deps.storage, &msg.whitelist)?; + let res = + cw_ics721_governance::instantiate(deps, info, msg.owner, msg.origin, msg.transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + msg.whitelist + .map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + + #[entry_point] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + match msg { + ExecuteMsg::Governance(Action::BridgeNft { + collection, + token_id, + msg, + }) => execute_bridge_nft(deps, env, info, collection, token_id, msg), + ExecuteMsg::Governance(action) => { + Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + } + ExecuteMsg::ReceiveNft(msg) => execute_receive_nft(deps, env, info, msg), + ExecuteMsg::AddToWhitelist { value } => { + execute_add_to_whitelist(deps, env, info, &value) + } + ExecuteMsg::RemoveFromWhitelist { value } => { + execute_remove_from_whitelist(deps, env, info, &value) + } + ExecuteMsg::ClearWhitelist() => execute_clear_whitelist(deps, env, info), + } + } + + #[entry_point] + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Governance() => cw_ics721_governance::query_governance(deps.storage), + QueryMsg::Whitelist {} => to_binary(&WHITELIST.query_whitelist(deps.storage)?), + QueryMsg::Whitelisted { value } => { + to_binary(&WHITELIST.query_is_whitelisted(deps.storage, &value)?) + } + } + } + + #[entry_point] + pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + match msg { + MigrateMsg::WithUpdate { + whitelist, + transfer_fee, + origin, + owner, + } => { + if let Some(list) = whitelist.clone() { + list.iter() + .map(|item| WHITELIST.add(deps.storage, &item.to_string())) + .collect::>>()?; + } + let res = cw_ics721_governance::migrate(deps, owner, origin, transfer_fee)?; + Ok(res.add_attribute( + "whitelist".to_string(), + whitelist.map_or("none".to_string(), |w| format!("{:?}", w)), + )) + } + } + } +} + +pub fn execute_add_to_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + value: &String, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.add(deps.storage, value)?; + Ok(Response::default() + .add_attribute("method", "execute_add_to_whitelist") + .add_attribute("value", value.to_string())) +} + +pub fn execute_remove_from_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, + value: &String, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.remove(deps.storage, value)?; + Ok(Response::default() + .add_attribute("method", "execute_remove_from_whitelist") + .add_attribute("value", value.to_string())) +} + +pub fn execute_clear_whitelist( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + WHITELIST.clear(deps.storage)?; + Ok(Response::default().add_attribute("method", "execute_clear_whitelist")) +} + +pub fn execute_receive_nft( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: Cw721ReceiveMsg, +) -> Result { + is_whitelisted(deps.storage, info.sender.to_string())?; + Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) +} + +pub fn execute_bridge_nft( + deps: DepsMut, + env: Env, + info: MessageInfo, + collection: String, + token_id: String, + msg: Binary, +) -> Result { + is_whitelisted(deps.storage, collection.to_string())?; + Ok(cw_ics721_governance::execute_bridge_nft( + deps, env, &info, collection, token_id, msg, + )?) +} + +pub fn is_whitelisted(storage: &dyn Storage, requestee: String) -> Result<(), ContractError> { + match WHITELIST.query_is_whitelisted(storage, &requestee)? { + true => Ok(()), + false => Err(ContractError::NotWhitelisted { requestee }), + } +} diff --git a/contracts/cw721-governed-collection-proxy/src/msg.rs b/contracts/cw721-governed-collection-proxy/src/msg.rs new file mode 100644 index 0000000..47e30ca --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/src/msg.rs @@ -0,0 +1,40 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Coin; +use cw_ics721_governance::{cw_ics721_governance_execute, cw_ics721_governance_query}; + +#[cw_serde] +pub struct InstantiateMsg { + pub origin: Option, + pub owner: Option, + pub transfer_fee: Option, + pub whitelist: Option>, +} + +#[cw_ics721_governance_execute] +#[cw_serde] +pub enum ExecuteMsg { + AddToWhitelist { value: String }, + RemoveFromWhitelist { value: String }, + ClearWhitelist(), +} + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Vec)] + Whitelist {}, + + #[returns(bool)] + Whitelisted { value: String }, +} + +#[cw_serde] +pub enum MigrateMsg { + WithUpdate { + origin: Option, + owner: Option, + transfer_fee: Option, + whitelist: Option>, + }, +} diff --git a/contracts/cw721-governed-collection-proxy/src/state.rs b/contracts/cw721-governed-collection-proxy/src/state.rs new file mode 100644 index 0000000..c71bd89 --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/src/state.rs @@ -0,0 +1,6 @@ +use cw721_whitelist::Whitelist; + +pub const CONTRACT_NAME: &str = "crates.io:cw721-governed-collection-proxy"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const WHITELIST: Whitelist = Whitelist::new(); diff --git a/contracts/cw721-governed-collection-proxy/src/tests.rs b/contracts/cw721-governed-collection-proxy/src/tests.rs new file mode 100644 index 0000000..1cf6c8f --- /dev/null +++ b/contracts/cw721-governed-collection-proxy/src/tests.rs @@ -0,0 +1,416 @@ +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty, StdResult}; +use cw721_proxy_multi_test::Test as GovernedMultiTest; +use cw_ics721_governance::{Action, GovernanceError}; +use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; + +use crate::{ + entry, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +fn proxy_code() -> Box> { + let contract = ContractWrapper::new(entry::execute, entry::instantiate, entry::query); + Box::new(contract) +} + +pub struct Test { + pub governed_multi_test: GovernedMultiTest, + pub proxy_code_id: u64, + pub proxy: Addr, +} + +impl Test { + pub fn new( + cw721s: usize, + transfer_fee: Option, + set_owner: bool, + whitelist: Option>, + ) -> Self { + let mut governed_multi_test = GovernedMultiTest::new(cw721s, transfer_fee); + let proxy_code_id = governed_multi_test.app.store_code(proxy_code()); + let owner = match set_owner { + true => Some(governed_multi_test.minter.to_string()), + false => None, + }; + let proxy = governed_multi_test + .app + .instantiate_contract( + proxy_code_id, + governed_multi_test.minter.clone(), + &InstantiateMsg { + origin: Some(governed_multi_test.mock_receiver.to_string()), + owner, + transfer_fee: governed_multi_test.transfer_fee.clone(), + whitelist, + }, + &[], + "governed proxy", + None, + ) + .unwrap(); + Self { + governed_multi_test, + proxy_code_id, + proxy, + } + } + + pub fn add_to_whitelist( + &mut self, + owner: Addr, + collection: String, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::AddToWhitelist { value: collection }, + &[], + )?; + Ok(res) + } + + pub fn remove_from_whitelist( + &mut self, + owner: Addr, + collection: String, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::RemoveFromWhitelist { value: collection }, + &[], + )?; + Ok(res) + } + + pub fn clear_whitelist(&mut self, owner: Addr) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::ClearWhitelist(), + &[], + )?; + Ok(res) + } + + pub fn query_whitelist(&self) -> StdResult> { + // in case proxy passed message to origin + self.governed_multi_test + .app + .wrap() + .query_wasm_smart(&self.proxy, &QueryMsg::Whitelist {}) + } + + pub fn bridge_nft( + &mut self, + sender: Addr, + proxy: Addr, + collection: Addr, + token_id: String, + channel_id: String, + transfer_fee: Option, + ) -> Result { + let funds = transfer_fee.map_or(vec![], |fee| vec![fee]); + let res = self.governed_multi_test.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::BridgeNft { + collection: collection.to_string(), + token_id, + msg: to_binary(&self.governed_multi_test.ibc_outgoing_msg(channel_id))?, + }), + &funds, + )?; + + Ok(res) + } +} + +#[test] +fn add_to_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + assert_eq!(test.query_whitelist().unwrap(), Vec::::new()); + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721s[0].to_string(), + ) + .unwrap(); + assert_eq!( + test.query_whitelist().unwrap(), + vec![test.governed_multi_test.cw721s[0].to_string()] + ); +} + +#[test] +fn add_to_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .add_to_whitelist( + Addr::unchecked("unauthorized"), + test.governed_multi_test.cw721s[0].to_string(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn add_to_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .add_to_whitelist( + Addr::unchecked("unauthorized"), + test.governed_multi_test.cw721s[0].to_string(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn remove_from_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let whitelist = vec!["any".to_string()]; + let mut test = Test::new(1, transfer_fee, true, Some(whitelist.clone())); + assert_eq!(test.query_whitelist().unwrap(), whitelist,); + test.remove_from_whitelist(test.governed_multi_test.minter.clone(), "any".to_string()) + .unwrap(); + assert_eq!(test.query_whitelist().unwrap(), Vec::::new(),); +} + +#[test] +fn remove_from_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .remove_from_whitelist(Addr::unchecked("unauthorized"), "any".to_string()) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn remove_from_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .remove_from_whitelist( + Addr::unchecked("unauthorized"), + test.governed_multi_test.cw721s[0].to_string(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +#[test] +fn clear_whitelist_owner() { + let transfer_fee = Some(coin(100, "uark")); + let whitelist = vec!["any".to_string()]; + let mut test = Test::new(1, transfer_fee, true, Some(whitelist.clone())); + assert_eq!(test.query_whitelist().unwrap(), whitelist,); + + test.clear_whitelist(test.governed_multi_test.minter.clone()) + .unwrap(); + assert_eq!(test.query_whitelist().unwrap(), Vec::::new(),) +} + +#[test] +fn clear_whitelist_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, false, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn clear_whitelist_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, true, None); + let err: ContractError = test + .clear_whitelist(Addr::unchecked("unauthorized")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +//-- from governed test, test bridge and send nft again, due to new whitelist logic + +#[test] +fn bridge_nft_no_transfer_fee_whitelisted() { + let mut test = Test::new(1, None, true, None); + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721s[0].to_string(), + ) + .unwrap(); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + test.bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary( + &test + .governed_multi_test + .ibc_outgoing_msg(channel.to_string()) + ) + .unwrap(), + } + ) + } + } +} + +#[test] +fn send_nft_no_transfer_fee_whitelisted() { + let mut test = Test::new(1, None, true, None); + test.add_to_whitelist( + test.governed_multi_test.minter.clone(), + test.governed_multi_test.cw721s[0].to_string(), + ) + .unwrap(); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + test.governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary(&test.governed_multi_test.ibc_outgoing_msg("any".to_string())) + .unwrap(), + } + ) + } + } +} + +#[test] +fn bridge_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: test.governed_multi_test.cw721s[0].to_string() + } + ) +} + +#[test] +fn send_nft_no_transfer_fee_not_whitelisted() { + let mut test = Test::new(1, None, true, None); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::NotWhitelisted { + requestee: test.governed_multi_test.cw721s[0].to_string() + } + ) +} +// ---- diff --git a/contracts/cw721-governed-proxy/.cargo/config b/contracts/cw721-governed-proxy/.cargo/config new file mode 100644 index 0000000..4febd2d --- /dev/null +++ b/contracts/cw721-governed-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/cw721-governed-proxy/.gitignore b/contracts/cw721-governed-proxy/.gitignore new file mode 100644 index 0000000..dfdaaa6 --- /dev/null +++ b/contracts/cw721-governed-proxy/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/cw721-governed-proxy/Cargo.toml b/contracts/cw721-governed-proxy/Cargo.toml new file mode 100644 index 0000000..5fc23e2 --- /dev/null +++ b/contracts/cw721-governed-proxy/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "cw721-governed-proxy" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +cw721-proxy = { workspace = true } +cw-ics721-governance = { workspace = true} +cw-utils = { workspace = true } +ibc-outgoing-msg = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-proxy-tester = { workspace = true } +cw721-proxy-multi-test = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/cw721-governed-proxy/schema/cw721-governed-proxy.json b/contracts/cw721-governed-proxy/schema/cw721-governed-proxy.json new file mode 100644 index 0000000..35ea777 --- /dev/null +++ b/contracts/cw721-governed-proxy/schema/cw721-governed-proxy.json @@ -0,0 +1,326 @@ +{ + "contract_name": "cw721-governed-proxy", + "contract_version": "0.0.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "properties": { + "origin": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Actions that can be taken to alter the proxy contract's governance like in `execute`'s entry point: ```rust use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; use cosmwasm_schema::cw_serde; use cosmwasm_std::entry_point;\n\n#[cw_serde] pub enum ExecuteMsg { Governance(cw_ics721_governance::Action), ReceiveNft(cw721::Cw721ReceiveMsg) }\n\n#[cfg_attr(not(feature = \"library\"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::Governance(action) => { Ok(cw_ics721_governance::execute(deps, env, &info, action)?) } ExecuteMsg::ReceiveNft(msg) => { Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) } } } ```", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Cw721ReceiveMsg to be forwared to ICS721 (origin). NOTE: this is NOT part of governance, since it is send by cw721 contract diretly to proxy", + "type": "object", + "required": [ + "receive_nft" + ], + "properties": { + "receive_nft": { + "$ref": "#/definitions/Cw721ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the proxy contract's governance", + "oneOf": [ + { + "description": "Changing owner of proxy. Once set, it can't be set to None - except via migration.", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "ICS721 contract where Cw721ReceiveMsg is forwarded to.", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds!", + "type": "object", + "required": [ + "transfer_fee" + ], + "properties": { + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "description": "Send funds from proxy to specified address.", + "type": "object", + "required": [ + "send_funds" + ], + "properties": { + "send_funds": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721.", + "type": "object", + "required": [ + "bridge_nft" + ], + "properties": { + "bridge_nft": { + "type": "object", + "required": [ + "collection", + "msg", + "token_id" + ], + "properties": { + "collection": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Cw721ReceiveMsg": { + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "msg", + "sender", + "token_id" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Query the contract's governance information", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "governance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Governance", + "description": "A governed contract may have: - an optional owner, - an origin (ICS721) where msgs are forwarded to, and - an optional transfer fee.\n\nOwner: - used in assert_owner(). For example execute_transfer_fee() allows only owner to change fees.\n\nOrigin: - ...\n\nTransfer Fee: - ...", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "$ref": "#/definitions/Addr" + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/cw721-governed-proxy/src/bin/schema.rs b/contracts/cw721-governed-proxy/src/bin/schema.rs new file mode 100644 index 0000000..10fbaaf --- /dev/null +++ b/contracts/cw721-governed-proxy/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw721_governed_proxy::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/cw721-governed-proxy/src/error.rs b/contracts/cw721-governed-proxy/src/error.rs new file mode 100644 index 0000000..709d84f --- /dev/null +++ b/contracts/cw721-governed-proxy/src/error.rs @@ -0,0 +1,8 @@ +use cw_ics721_governance::GovernanceError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Governance(#[from] GovernanceError), +} diff --git a/contracts/cw721-governed-proxy/src/lib.rs b/contracts/cw721-governed-proxy/src/lib.rs new file mode 100644 index 0000000..cec47c0 --- /dev/null +++ b/contracts/cw721-governed-proxy/src/lib.rs @@ -0,0 +1,75 @@ +pub mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +pub mod entry { + use crate::{ + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + state::{CONTRACT_NAME, CONTRACT_VERSION}, + }; + + #[cfg(not(feature = "library"))] + use cosmwasm_std::entry_point; + use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + use cw2::set_contract_version; + + // This makes a conscious choice on the various generics used by the contract + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + cw_ics721_governance::instantiate(deps, info, msg.owner, msg.origin, msg.transfer_fee) + } + + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + match msg { + ExecuteMsg::Governance(action) => { + Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + } + ExecuteMsg::ReceiveNft(msg) => { + Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) + } + } + } + + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Governance() => cw_ics721_governance::query_governance(deps.storage), + } + } + + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + match msg { + MigrateMsg::WithUpdate { + origin, + owner, + transfer_fee, + } => { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(cw_ics721_governance::migrate( + deps, + owner, + origin, + transfer_fee, + )?) + } + } + } +} diff --git a/contracts/cw721-governed-proxy/src/msg.rs b/contracts/cw721-governed-proxy/src/msg.rs new file mode 100644 index 0000000..4b69f89 --- /dev/null +++ b/contracts/cw721-governed-proxy/src/msg.rs @@ -0,0 +1,28 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Coin; +use cw_ics721_governance::{cw_ics721_governance_execute, cw_ics721_governance_query}; + +#[cw_serde] +pub struct InstantiateMsg { + pub origin: Option, + pub owner: Option, + pub transfer_fee: Option, +} + +#[cw_ics721_governance_execute] +#[cw_serde] +pub enum ExecuteMsg {} + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg {} + +#[cw_serde] +pub enum MigrateMsg { + WithUpdate { + origin: Option, + owner: Option, + transfer_fee: Option, + }, +} diff --git a/contracts/cw721-governed-proxy/src/state.rs b/contracts/cw721-governed-proxy/src/state.rs new file mode 100644 index 0000000..4303b40 --- /dev/null +++ b/contracts/cw721-governed-proxy/src/state.rs @@ -0,0 +1,3 @@ +// Version info for migration +pub const CONTRACT_NAME: &str = "crates.io:cw721-governed-proxy"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/contracts/cw721-governed-proxy/src/tests.rs b/contracts/cw721-governed-proxy/src/tests.rs new file mode 100644 index 0000000..51c6481 --- /dev/null +++ b/contracts/cw721-governed-proxy/src/tests.rs @@ -0,0 +1,448 @@ +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty}; +use cw721_proxy_multi_test::Test as GovernedMultiTest; +use cw_multi_test::{Contract, ContractWrapper, Executor}; + +use crate::{entry, error::ContractError, msg::InstantiateMsg}; + +fn proxy_code() -> Box> { + let contract = ContractWrapper::new(entry::execute, entry::instantiate, entry::query); + Box::new(contract) +} + +pub struct Test { + pub governed_multi_test: GovernedMultiTest, + pub proxy_code_id: u64, + pub proxy: Addr, +} + +impl Test { + pub fn new(cw721s: usize, transfer_fee: Option, set_owner: bool) -> Self { + let mut governed_multi_test = GovernedMultiTest::new(cw721s, transfer_fee); + let proxy_code_id = governed_multi_test.app.store_code(proxy_code()); + let owner = match set_owner { + true => Some(governed_multi_test.minter.to_string()), + false => None, + }; + let proxy = governed_multi_test + .app + .instantiate_contract( + proxy_code_id, + governed_multi_test.minter.clone(), + &InstantiateMsg { + origin: Some(governed_multi_test.mock_receiver.to_string()), + owner, + transfer_fee: governed_multi_test.transfer_fee.clone(), + }, + &[], + "governed proxy", + None, + ) + .unwrap(); + Self { + governed_multi_test, + proxy_code_id, + proxy, + } + } +} + +#[test] +fn bridge_nft_no_transfer_fee() { + let mut test = Test::new(1, None, false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + test.governed_multi_test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy, + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary( + &test + .governed_multi_test + .ibc_outgoing_msg(channel.to_string()) + ) + .unwrap(), + } + ) + } + } +} + +#[test] +fn send_nft_no_transfer_fee() { + let mut test = Test::new(1, None, false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + test.governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary(&test.governed_multi_test.ibc_outgoing_msg("any".to_string())) + .unwrap(), + } + ) + } + } +} + +#[test] +fn bridge_nft_unapproved() { + let mut test = Test::new(1, None, false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(cw_ics721_governance::GovernanceError::MissingApproval { + spender: test.proxy.to_string(), + collection: test.governed_multi_test.cw721s[0].to_string(), + token: token_id, + }) + ) +} + +#[test] +fn bridge_nft_no_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy, + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance( + cw_ics721_governance::GovernanceError::IncorrectPaymentAmount( + coin(0, "uark"), + transfer_fee + ) + ) + ) +} + +#[test] +fn send_nft_no_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance( + cw_ics721_governance::GovernanceError::IncorrectPaymentAmount( + coin(0, "uark"), + transfer_fee + ) + ) + ) +} + +#[test] +fn bridge_nft_correct_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + test.governed_multi_test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy, + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap(); +} + +#[test] +fn send_nft_correct_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), // paid to collection, but proxy needs it! + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance( + cw_ics721_governance::GovernanceError::IncorrectPaymentAmount( + coin(0, "uark"), + transfer_fee + ) + ) // proxy receive 0 coins + ) +} + +#[test] +fn bridge_nft_insufficient_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy, + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + Some(coin(50, "uark")), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance( + cw_ics721_governance::GovernanceError::IncorrectPaymentAmount( + coin(50, "uark"), + transfer_fee + ) + ) + ) +} + +#[test] +fn send_nft_insufficient_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + Some(coin(50, "uark")), // paid to collection, but proxy needs it! + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance( + cw_ics721_governance::GovernanceError::IncorrectPaymentAmount( + coin(0, "uark"), + transfer_fee + ) + ) // proxy receive 0 coins + ) +} + +#[test] +fn bridge_nft_higher_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy, + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + Some(coin(150, "uark")), + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance( + cw_ics721_governance::GovernanceError::IncorrectPaymentAmount( + coin(150, "uark"), + transfer_fee + ) + ) + ) +} + +#[test] +fn send_nft_higher_payment() { + let transfer_fee = coin(100, "uark"); + let mut test = Test::new(1, Some(transfer_fee.clone()), false); + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + let channel = "any"; + let err: ContractError = test + .governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + Some(coin(150, "uark")), // paid to collection, but proxy needs it! + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance( + cw_ics721_governance::GovernanceError::IncorrectPaymentAmount( + coin(0, "uark"), + transfer_fee + ) + ) // proxy receive 0 coins + ) +} diff --git a/contracts/cw721-governed-rate-limited-proxy/.cargo/config b/contracts/cw721-governed-rate-limited-proxy/.cargo/config new file mode 100644 index 0000000..4febd2d --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/cw721-governed-rate-limited-proxy/.gitignore b/contracts/cw721-governed-rate-limited-proxy/.gitignore new file mode 100644 index 0000000..dfdaaa6 --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/cw721-governed-rate-limited-proxy/Cargo.toml b/contracts/cw721-governed-rate-limited-proxy/Cargo.toml new file mode 100644 index 0000000..a196f9c --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "cw721-governed-rate-limited-proxy" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-proxy = { workspace = true } +cw721-proxy-derive = { workspace = true } +cw-rate-limiter = { workspace = true } +cw721-governed-proxy = { workspace = true, features = ["library"] } +cw-ics721-governance = { workspace = true} +ibc-outgoing-msg = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-base = { workspace = true } +cw721-proxy-tester = { workspace = true } +cw721-proxy-multi-test = { workspace = true } +rand = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/cw721-governed-rate-limited-proxy/rustfmt.toml b/contracts/cw721-governed-rate-limited-proxy/rustfmt.toml new file mode 100644 index 0000000..11a85e6 --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/rustfmt.toml @@ -0,0 +1,15 @@ +# stable +newline_style = "unix" +hard_tabs = false +tab_spaces = 4 + +# unstable... should we require `rustup run nightly cargo fmt` ? +# or just update the style guide when they are stable? +#fn_single_line = true +#format_code_in_doc_comments = true +#overflow_delimited_expr = true +#reorder_impl_items = true +#struct_field_align_threshold = 20 +#struct_lit_single_line = true +#report_todo = "Always" + diff --git a/contracts/cw721-governed-rate-limited-proxy/schema/cw721-governed-rate-limited-proxy.json b/contracts/cw721-governed-rate-limited-proxy/schema/cw721-governed-rate-limited-proxy.json new file mode 100644 index 0000000..99b8b64 --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/schema/cw721-governed-rate-limited-proxy.json @@ -0,0 +1,456 @@ +{ + "contract_name": "cw721-governed-rate-limited-proxy", + "contract_version": "0.0.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "rate_limit" + ], + "properties": { + "origin": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "rate_limit": { + "$ref": "#/definitions/Rate" + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Rate": { + "oneOf": [ + { + "type": "object", + "required": [ + "per_block" + ], + "properties": { + "per_block": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "blocks" + ], + "properties": { + "blocks": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "rate_limit" + ], + "properties": { + "rate_limit": { + "$ref": "#/definitions/Rate" + } + }, + "additionalProperties": false + }, + { + "description": "Actions that can be taken to alter the proxy contract's governance like in `execute`'s entry point: ```rust use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; use cosmwasm_schema::cw_serde; use cosmwasm_std::entry_point;\n\n#[cw_serde] pub enum ExecuteMsg { Governance(cw_ics721_governance::Action), ReceiveNft(cw721::Cw721ReceiveMsg) }\n\n#[cfg_attr(not(feature = \"library\"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::Governance(action) => { Ok(cw_ics721_governance::execute(deps, env, &info, action)?) } ExecuteMsg::ReceiveNft(msg) => { Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) } } } ```", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Cw721ReceiveMsg to be forwared to ICS721 (origin). NOTE: this is NOT part of governance, since it is send by cw721 contract diretly to proxy", + "type": "object", + "required": [ + "receive_nft" + ], + "properties": { + "receive_nft": { + "$ref": "#/definitions/Cw721ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the proxy contract's governance", + "oneOf": [ + { + "description": "Changing owner of proxy. Once set, it can't be set to None - except via migration.", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "ICS721 contract where Cw721ReceiveMsg is forwarded to.", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds!", + "type": "object", + "required": [ + "transfer_fee" + ], + "properties": { + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "description": "Send funds from proxy to specified address.", + "type": "object", + "required": [ + "send_funds" + ], + "properties": { + "send_funds": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721.", + "type": "object", + "required": [ + "bridge_nft" + ], + "properties": { + "bridge_nft": { + "type": "object", + "required": [ + "collection", + "msg", + "token_id" + ], + "properties": { + "collection": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Cw721ReceiveMsg": { + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "msg", + "sender", + "token_id" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Rate": { + "oneOf": [ + { + "type": "object", + "required": [ + "per_block" + ], + "properties": { + "per_block": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "blocks" + ], + "properties": { + "blocks": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Gets the contract's rate limit.", + "type": "object", + "required": [ + "rate_limit" + ], + "properties": { + "rate_limit": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's governance information", + "type": "object", + "required": [ + "governance" + ], + "properties": { + "governance": { + "type": "array", + "items": [], + "maxItems": 0, + "minItems": 0 + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "governance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Governance", + "description": "A governed contract may have: - an optional owner, - an origin (ICS721) where msgs are forwarded to, and - an optional transfer fee.\n\nOwner: - used in assert_owner(). For example execute_transfer_fee() allows only owner to change fees.\n\nOrigin: - ...\n\nTransfer Fee: - ...", + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "$ref": "#/definitions/Addr" + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "transfer_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "rate_limit": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Rate", + "oneOf": [ + { + "type": "object", + "required": [ + "per_block" + ], + "properties": { + "per_block": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "blocks" + ], + "properties": { + "blocks": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/cw721-governed-rate-limited-proxy/src/bin/schema.rs b/contracts/cw721-governed-rate-limited-proxy/src/bin/schema.rs new file mode 100644 index 0000000..67544ed --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw721_governed_rate_limited_proxy::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/cw721-governed-rate-limited-proxy/src/error.rs b/contracts/cw721-governed-rate-limited-proxy/src/error.rs new file mode 100644 index 0000000..aae0668 --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/src/error.rs @@ -0,0 +1,18 @@ +use cosmwasm_std::StdError; +use cw_ics721_governance::GovernanceError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Governance(#[from] GovernanceError), + + #[error(transparent)] + Rate(#[from] cw_rate_limiter::RateLimitError), + + #[error("rate must be non-zero")] + ZeroRate {}, +} diff --git a/contracts/cw721-governed-rate-limited-proxy/src/lib.rs b/contracts/cw721-governed-rate-limited-proxy/src/lib.rs new file mode 100644 index 0000000..3398952 --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/src/lib.rs @@ -0,0 +1,155 @@ +use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo, Response}; +use cw721::Cw721ReceiveMsg; +use cw_rate_limiter::Rate; +use error::ContractError; +use state::RATE_LIMITER; + +pub mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +#[cfg(not(feature = "library"))] +pub mod entry { + use crate::error::ContractError; + use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + use crate::state::{CONTRACT_NAME, CONTRACT_VERSION, RATE_LIMITER}; + use crate::{execute_bridge_nft, execute_rate_limit, execute_receive_nft}; + + use cosmwasm_std::{entry_point, to_binary}; + use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + use cw2::set_contract_version; + use cw_ics721_governance::Action; + use cw_rate_limiter::Rate; + + // This makes a conscious choice on the various generics used by the contract + #[entry_point] + pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + if msg.rate_limit.is_zero() { + Err(ContractError::ZeroRate {}) + } else { + let (rate, units) = match msg.rate_limit { + Rate::PerBlock(rate) => (rate, "nfts_per_block"), + Rate::Blocks(rate) => (rate, "blocks_per_nft"), + }; + RATE_LIMITER.init(deps.storage, &msg.rate_limit)?; + let res = cw_ics721_governance::instantiate( + deps, + info, + msg.owner, + msg.origin, + msg.transfer_fee, + )?; + Ok(res + .add_attribute("rate".to_string(), rate.to_string()) + .add_attribute("units", units)) + } + } + + #[entry_point] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + match msg { + ExecuteMsg::Governance(Action::BridgeNft { + collection, + token_id, + msg, + }) => execute_bridge_nft(deps, env, info, collection, token_id, msg), + ExecuteMsg::Governance(action) => { + Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + } + ExecuteMsg::ReceiveNft(msg) => execute_receive_nft(deps, env, info, msg), + ExecuteMsg::RateLimit(rate) => execute_rate_limit(deps, env, info, rate), + } + } + + #[entry_point] + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Governance() => cw_ics721_governance::query_governance(deps.storage), + QueryMsg::RateLimit {} => to_binary(&RATE_LIMITER.query_limit(deps.storage)?), + } + } + + #[entry_point] + pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + match msg { + MigrateMsg::WithUpdate { + origin, + owner, + transfer_fee, + rate_limit, + } => { + if let Some(rate) = rate_limit { + if rate.is_zero() { + return Err(ContractError::ZeroRate {}); + } else { + RATE_LIMITER.init(deps.storage, &rate)?; + } + } + let res = cw_ics721_governance::migrate(deps, owner, origin, transfer_fee)?; + Ok(res.add_attribute("rate_limit", format!("{:?}", rate_limit))) + } + } + } +} + +pub fn execute_rate_limit( + deps: DepsMut, + _env: Env, + info: MessageInfo, + rate_limit: Rate, +) -> Result { + cw_ics721_governance::assert_owner(deps.storage, &info.sender)?; + if rate_limit.is_zero() { + Err(ContractError::ZeroRate {}) + } else { + RATE_LIMITER.init(deps.storage, &rate_limit)?; + let (rate, units) = match rate_limit { + Rate::PerBlock(rate) => (rate, "nfts_per_block"), + Rate::Blocks(rate) => (rate, "blocks_per_nft"), + }; + Ok(Response::default() + .add_attribute("method", "execute_rate_limit") + .add_attribute("rate", rate.to_string()) + .add_attribute("units", units)) + } +} + +pub fn execute_receive_nft( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw721ReceiveMsg, +) -> Result { + RATE_LIMITER.limit(deps.storage, &env, info.sender.as_str())?; + Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) +} + +pub fn execute_bridge_nft( + deps: DepsMut, + env: Env, + info: MessageInfo, + collection: String, + token_id: String, + msg: Binary, +) -> Result { + RATE_LIMITER.limit(deps.storage, &env, info.sender.as_str())?; + Ok(cw_ics721_governance::execute_bridge_nft( + deps, env, &info, collection, token_id, msg, + )?) +} diff --git a/contracts/cw721-governed-rate-limited-proxy/src/msg.rs b/contracts/cw721-governed-rate-limited-proxy/src/msg.rs new file mode 100644 index 0000000..84612eb --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/src/msg.rs @@ -0,0 +1,39 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use cw_rate_limiter::Rate; + +use cosmwasm_std::Coin; +use cw_ics721_governance::{cw_ics721_governance_execute, cw_ics721_governance_query}; + +#[cw_serde] +pub struct InstantiateMsg { + pub origin: Option, + pub owner: Option, + pub transfer_fee: Option, + pub rate_limit: Rate, +} + +#[cw_ics721_governance_execute] +#[cw_serde] +pub enum ExecuteMsg { + RateLimit(Rate), +} + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Gets the contract's rate limit. + #[returns(Rate)] + RateLimit {}, +} + +#[cw_serde] +pub enum MigrateMsg { + WithUpdate { + origin: Option, + owner: Option, + transfer_fee: Option, + rate_limit: Option, + }, +} diff --git a/contracts/cw721-governed-rate-limited-proxy/src/state.rs b/contracts/cw721-governed-rate-limited-proxy/src/state.rs new file mode 100644 index 0000000..499c9af --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/src/state.rs @@ -0,0 +1,6 @@ +use cw_rate_limiter::RateLimiter; + +pub const CONTRACT_NAME: &str = "crates.io:cw721-proxy-code-id"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const RATE_LIMITER: RateLimiter = RateLimiter::new("rate_limit", "sender"); diff --git a/contracts/cw721-governed-rate-limited-proxy/src/tests.rs b/contracts/cw721-governed-rate-limited-proxy/src/tests.rs new file mode 100644 index 0000000..d19a75f --- /dev/null +++ b/contracts/cw721-governed-rate-limited-proxy/src/tests.rs @@ -0,0 +1,227 @@ +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty}; +use cw721_proxy_multi_test::Test as GovernedMultiTest; +use cw_ics721_governance::{Action, GovernanceError}; +use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; +use cw_rate_limiter::Rate; + +use crate::{ + entry, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg}, +}; + +fn proxy_code() -> Box> { + let contract = ContractWrapper::new(entry::execute, entry::instantiate, entry::query); + Box::new(contract) +} + +pub struct Test { + pub governed_multi_test: GovernedMultiTest, + pub proxy_code_id: u64, + pub proxy: Addr, +} + +impl Test { + pub fn new( + cw721s: usize, + transfer_fee: Option, + rate_limit: Rate, + set_owner: bool, + ) -> Self { + let mut governed_multi_test = GovernedMultiTest::new(cw721s, transfer_fee); + let proxy_code_id = governed_multi_test.app.store_code(proxy_code()); + let owner = match set_owner { + true => Some(governed_multi_test.minter.to_string()), + false => None, + }; + let proxy = governed_multi_test + .app + .instantiate_contract( + proxy_code_id, + governed_multi_test.minter.clone(), + &InstantiateMsg { + origin: Some(governed_multi_test.mock_receiver.to_string()), + owner, + transfer_fee: governed_multi_test.transfer_fee.clone(), + rate_limit, + }, + &[], + "governed proxy", + None, + ) + .unwrap(); + Self { + governed_multi_test, + proxy_code_id, + proxy, + } + } + + pub fn execute_rate_limit( + &mut self, + owner: Addr, + rate_limit: Rate, + ) -> Result { + let res = self.governed_multi_test.app.execute_contract( + owner.clone(), + self.proxy.clone(), + &ExecuteMsg::RateLimit(rate_limit), + &[], + )?; + Ok(res) + } + + pub fn bridge_nft( + &mut self, + sender: Addr, + proxy: Addr, + collection: Addr, + token_id: String, + channel_id: String, + transfer_fee: Option, + ) -> Result { + let funds = transfer_fee.map_or(vec![], |fee| vec![fee]); + let res = self.governed_multi_test.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::BridgeNft { + collection: collection.to_string(), + token_id, + msg: to_binary(&self.governed_multi_test.ibc_outgoing_msg(channel_id))?, + }), + &funds, + )?; + + Ok(res) + } +} + +#[test] +fn rate_limit_is_zero() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, Rate::Blocks(1), true); + let err: ContractError = test + .execute_rate_limit(test.governed_multi_test.minter.clone(), Rate::PerBlock(0)) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::ZeroRate {}) +} + +#[test] +fn rate_limit_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, Rate::Blocks(1), true); + test.execute_rate_limit(test.governed_multi_test.minter.clone(), Rate::Blocks(1)) + .unwrap(); +} + +#[test] +fn rate_limit_no_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, Rate::Blocks(1), false); + let err: ContractError = test + .execute_rate_limit(Addr::unchecked("unauthorized"), Rate::Blocks(1)) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Governance(GovernanceError::NoOwner)) +} + +#[test] +fn rate_limit_not_owner() { + let transfer_fee = Some(coin(100, "uark")); + let mut test = Test::new(1, transfer_fee, Rate::Blocks(1), true); + let err: ContractError = test + .execute_rate_limit(Addr::unchecked("unauthorized"), Rate::Blocks(1)) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::Governance(GovernanceError::NotOwner("unauthorized".to_string())) + ) +} + +//-- from governed test, test bridge and send nft again, due to new whitelist logic + +#[test] +fn bridge_nft_no_transfer_fee() { + let mut test = Test::new(1, None, Rate::Blocks(1), true); + let channel = "any"; + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + test.governed_multi_test + .approve( + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + test.proxy.to_string(), + ) + .unwrap(); + + test.bridge_nft( + test.governed_multi_test.minter.clone(), + test.proxy.clone(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + test.governed_multi_test.transfer_fee.clone(), + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary( + &test + .governed_multi_test + .ibc_outgoing_msg(channel.to_string()) + ) + .unwrap(), + } + ) + } + } +} + +#[test] +fn send_nft_no_transfer_fee() { + let mut test = Test::new(1, None, Rate::Blocks(1), true); + let channel = "any"; + let token_id = test + .governed_multi_test + .mint(test.governed_multi_test.cw721s[0].clone()) + .unwrap(); + + test.governed_multi_test + .send_nft( + test.governed_multi_test.minter.clone(), + test.proxy.to_string(), + test.governed_multi_test.cw721s[0].clone(), + token_id.clone(), + channel.to_string(), + None, + ) + .unwrap(); + match test.governed_multi_test.query_last_msg().unwrap() { + cw721_proxy_tester::msg::ExecuteMsg::ReceiveProxyNft { eyeball, msg } => { + assert_eq!(eyeball, test.governed_multi_test.cw721s[0].clone()); + assert_eq!( + msg, + cw721::Cw721ReceiveMsg { + sender: test.governed_multi_test.minter.to_string(), + token_id, + msg: to_binary(&test.governed_multi_test.ibc_outgoing_msg("any".to_string())) + .unwrap(), + } + ) + } + } +} +// ---- diff --git a/contracts/cw721-rate-limited-proxy/.cargo/config b/contracts/cw721-rate-limited-proxy/.cargo/config index 336b618..4febd2d 100644 --- a/contracts/cw721-rate-limited-proxy/.cargo/config +++ b/contracts/cw721-rate-limited-proxy/.cargo/config @@ -1,4 +1,4 @@ [alias] wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example schema" +schema = "run --bin schema" diff --git a/contracts/cw721-rate-limited-proxy/Cargo.toml b/contracts/cw721-rate-limited-proxy/Cargo.toml index 935527c..f7f7ae6 100644 --- a/contracts/cw721-rate-limited-proxy/Cargo.toml +++ b/contracts/cw721-rate-limited-proxy/Cargo.toml @@ -4,6 +4,7 @@ license = "BSD-3" authors = ["ekez "] edition = "2021" version = "0.0.1" +repository = "https://github.com/0xekez/cw721-proxy" [lib] crate-type = ["cdylib", "rlib"] @@ -15,19 +16,19 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cosmwasm-std = "1.1" -cosmwasm-schema = "1.1" -cw-storage-plus = "0.16" -cw2 = "0.16" -cw721 = "0.16" -cw721-proxy = { path = "../../packages/cw721-proxy", version = "*" } -cw721-proxy-derive = { path = "../../packages/cw721-proxy-derive", version = "*" } -cw-rate-limiter = { path = "../../packages/cw-rate-limiter", version = "*" } -thiserror = "1" +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-proxy = { workspace = true } +cw721-proxy-derive = { workspace = true } +cw-rate-limiter = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] -cw-multi-test = "0.16.0" -cw721-base = "0.16" -cw721-proxy-tester = { path = "../../debug/cw721-proxy-tester", version = "*" } -rand = "0.8" -anyhow = "1.0" +cw-multi-test = { workspace = true } +cw721-base = { workspace = true } +cw721-proxy-tester = { workspace = true } +rand = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/cw721-rate-limited-proxy/schema/cw721-rate-limited-proxy.json b/contracts/cw721-rate-limited-proxy/schema/cw721-rate-limited-proxy.json index 3ea339a..c4b904f 100644 --- a/contracts/cw721-rate-limited-proxy/schema/cw721-rate-limited-proxy.json +++ b/contracts/cw721-rate-limited-proxy/schema/cw721-rate-limited-proxy.json @@ -10,6 +10,12 @@ "rate_limit" ], "properties": { + "origin": { + "type": [ + "string", + "null" + ] + }, "rate_limit": { "$ref": "#/definitions/Rate" } @@ -25,7 +31,9 @@ ], "properties": { "per_block": { - "$ref": "#/definitions/Uint128" + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -37,16 +45,14 @@ ], "properties": { "blocks": { - "$ref": "#/definitions/Uint128" + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false } ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" } } }, @@ -112,12 +118,30 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, "migrate": null, "sudo": null, "responses": { + "origin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "String", + "type": "string" + }, "rate_limit": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Rate", @@ -129,7 +153,9 @@ ], "properties": { "per_block": { - "$ref": "#/definitions/Uint128" + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -141,18 +167,14 @@ ], "properties": { "blocks": { - "$ref": "#/definitions/Uint128" + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } + ] } } } diff --git a/contracts/cw721-rate-limited-proxy/src/contract.rs b/contracts/cw721-rate-limited-proxy/src/contract.rs index d6d6c90..abc1750 100644 --- a/contracts/cw721-rate-limited-proxy/src/contract.rs +++ b/contracts/cw721-rate-limited-proxy/src/contract.rs @@ -9,7 +9,7 @@ use cw721_proxy::ProxyExecuteMsg; use cw_rate_limiter::Rate; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::state::{ORIGIN, RATE_LIMIT}; const CONTRACT_NAME: &str = "crates.io:cw721-proxy-rate-limit"; @@ -81,3 +81,26 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Origin {} => to_binary(&ORIGIN.load(deps.storage)?), } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + // Set contract to latest version + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + match msg { + MigrateMsg::WithUpdate { origin, rate_limit } => { + if let Some(rate) = rate_limit { + if rate.is_zero() { + return Err(ContractError::ZeroRate {}); + } else { + RATE_LIMIT.init(deps.storage, &rate)?; + } + } + if let Some(origin) = origin { + let origin = deps.api.addr_validate(&origin)?; + ORIGIN.save(deps.storage, &origin)?; + } + Ok(Response::default().add_attribute("method", "migrate")) + } + } +} diff --git a/contracts/cw721-rate-limited-proxy/src/msg.rs b/contracts/cw721-rate-limited-proxy/src/msg.rs index bd0d8b7..4ce9886 100644 --- a/contracts/cw721-rate-limited-proxy/src/msg.rs +++ b/contracts/cw721-rate-limited-proxy/src/msg.rs @@ -23,3 +23,11 @@ pub enum QueryMsg { #[returns(String)] Origin {}, } + +#[cw_serde] +pub enum MigrateMsg { + WithUpdate { + rate_limit: Option, + origin: Option, + }, +} diff --git a/debug/cw721-proxy-tester/Cargo.toml b/debug/cw721-proxy-tester/Cargo.toml index 8f30cfa..d7d6475 100644 --- a/debug/cw721-proxy-tester/Cargo.toml +++ b/debug/cw721-proxy-tester/Cargo.toml @@ -13,12 +13,12 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cosmwasm-std = "1.1.1" -cosmwasm-schema = "1.1.1" -cw-storage-plus = "0.15.0" -cw2 = "0.15.0" -cw721 = "0.15.0" -cw721-proxy = { path = "../../packages/cw721-proxy", version = "*" } -cw721-proxy-derive = { path = "../../packages/cw721-proxy-derive", version = "*" } -thiserror = { version = "1" } -cw-multi-test = "0.15.0" +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-proxy = { workspace = true } +cw721-proxy-derive = { workspace = true } +thiserror = { workspace = true } +cw-multi-test = { workspace = true } diff --git a/packages/cw-ics721-governance/Cargo.toml b/packages/cw-ics721-governance/Cargo.toml new file mode 100644 index 0000000..34cf043 --- /dev/null +++ b/packages/cw-ics721-governance/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cw-ics721-governance" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +doctest = false # disable doc tests + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-ics721-governance-derive = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +cw721-proxy = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } diff --git a/packages/cw-ics721-governance/README.md b/packages/cw-ics721-governance/README.md new file mode 100644 index 0000000..e4ddb89 --- /dev/null +++ b/packages/cw-ics721-governance/README.md @@ -0,0 +1,130 @@ +# ICS721 Governance + +Utility for controlling governance of [ICS721 proxy](https://github.com/arkprotocol/cw721-proxy) smart contracts. + +## How to use + +Initialize the governed proxy during instantiation using the `instantiate` method provided by this crate: + +```rust +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; +use cw_ics721_governance::GovernanceError; + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + cw_ics721_governance::instantiate(deps, info, msg.owner, msg.origin, msg.transfer_fee)?; + Ok(Response::new()) +} +``` + +Use the `#[cw_ics721_governance_execute]` macro to extend your execute message: + +```rust +use cosmwasm_schema::cw_serde; +use cw_ics721_governance::cw_ics721_governance_execute; + +#[cw_ics721_governance_execute] +#[cw_serde] +enum ExecuteMsg { + Foo {}, + Bar {}, +} +``` + +The macro inserts 2 new variants, `Governance` and `ReceiveNft`, to the enum: + +```rust +#[cw_serde] +enum ExecuteMsg { + Governance(cw_ics721_governance::Action), + ReceiveNft(cw721::Cw721ReceiveMsg), + Foo {}, + Bar {}, +} +``` + +Where `Action` can be one of these: + +- Owner: changing owner of proxy. Once set, it can't be set to None - except via migration! +- Origin: ICS721 contract where Cw721ReceiveMsg is forwarded to. +- Transfer Fee: optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds! +- Send Funds: from proxy to specified address. +- Bridge NFT: analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721. + +Handle the messages using the `execute` function provided by this crate: + +```rust +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Governance(action) => { + Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + } + ExecuteMsg::ReceiveNft(msg) => { + Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) + } + } +} +``` + +Use the `#[cw_ics721_governance_query]` macro to extend your query message: + +```rust +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_ics721_governance::cw_ics721_governance_query; + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(FooResponse)] + Foo {}, + #[returns(BarResponse)] + Bar {}, +} +``` + +The macro inserts a new variant, `Governance`: + +```rust +#[cw_serde] +#[derive(QueryResponses)] +enum QueryMsg { + #[returns(Governance)] + Governance {}, + #[returns(FooResponse)] + Foo {}, + #[returns(BarResponse)] + Bar {}, +} +``` + +Handle the message using the `query_governance` function provided by this crate: + +```rust +use cosmwasm_std::{entry_point, Deps, Env, Binary}; +use cw_ics721_governance::query_governance; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Governance() => query_governance(deps.storage), + } +} +``` + +# Kudos + +This work has been adapted on the great work by Jake and Larry: +- DAO DAO: https://github.com/DA0-DA0/dao-contracts/blob/74bd3881fdd86829e5e8b132b9952dd64f2d0737/packages/dao-macros/src/lib.rs#L9 +- CW++: https://github.com/larry0x/cw-plus-plus/tree/main/packages/ownable \ No newline at end of file diff --git a/packages/cw-ics721-governance/derive/Cargo.toml b/packages/cw-ics721-governance/derive/Cargo.toml new file mode 100644 index 0000000..e074af1 --- /dev/null +++ b/packages/cw-ics721-governance/derive/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cw-ics721-governance-derive" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[lib] +doctest = false # disable doc tests +proc-macro = true + +[dependencies] +cosmwasm-std = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } diff --git a/packages/cw-ics721-governance/derive/README.md b/packages/cw-ics721-governance/derive/README.md new file mode 100644 index 0000000..a000813 --- /dev/null +++ b/packages/cw-ics721-governance/derive/README.md @@ -0,0 +1,3 @@ +# CW Ownable Derive + +Macros for generating code used by the [`cw-ics721-governance`](https://github.com/arkprotocol/cw721-proxy) smart contracts. \ No newline at end of file diff --git a/packages/cw-ics721-governance/derive/src/lib.rs b/packages/cw-ics721-governance/derive/src/lib.rs new file mode 100644 index 0000000..5debdc3 --- /dev/null +++ b/packages/cw-ics721-governance/derive/src/lib.rs @@ -0,0 +1,182 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, AttributeArgs, DataEnum, DeriveInput}; + +/// Merges the variants of two enums. +/// +/// Adapted from DAO DAO: +/// https://github.com/DA0-DA0/dao-contracts/blob/74bd3881fdd86829e5e8b132b9952dd64f2d0737/packages/dao-macros/src/lib.rs#L9 +fn merge_variants(metadata: TokenStream, left: TokenStream, right: TokenStream) -> TokenStream { + use syn::Data::Enum; + + // parse metadata + let args = parse_macro_input!(metadata as AttributeArgs); + if let Some(first_arg) = args.first() { + return syn::Error::new_spanned(first_arg, "macro takes no arguments") + .to_compile_error() + .into(); + } + + // parse the left enum + let mut left: DeriveInput = parse_macro_input!(left); + let Enum(DataEnum { + variants, + .. + }) = &mut left.data else { + return syn::Error::new(left.ident.span(), "only enums can accept variants") + .to_compile_error() + .into(); + }; + + // parse the right enum + let right: DeriveInput = parse_macro_input!(right); + let Enum(DataEnum { + variants: to_add, + .. + }) = right.data else { + return syn::Error::new(left.ident.span(), "only enums can provide variants") + .to_compile_error() + .into(); + }; + + // insert variants from the right to the left + variants.extend(to_add.into_iter()); + + quote! { #left }.into() +} + +/// Append governance-related execute message variant(s) to an enum. +/// +/// For example, apply the `cw_ics721_governance_execute` macro to the following enum: +/// +/// ```rust +/// use cosmwasm_schema::cw_serde; +/// use cw_ics721_governance::cw_ics721_governance_exeucte; +/// +/// #[cw_ics721_governance_execute] +/// #[cw_serde] +/// enum ExecuteMsg { +/// Foo {}, +/// Bar {}, +/// } +/// ``` +/// +/// Is equivalent to: +/// +/// ```rust +/// use cosmwasm_schema::cw_serde; +/// use cw_ics721_governance::Action; +/// +/// #[cw_serde] +/// enum ExecuteMsg { +/// Governance(Action), +/// ReceiveNft(cw721::Cw721ReceiveMsg), +/// Foo {}, +/// Bar {}, +/// } +/// ``` +/// +/// Note: `#[cw_ics721_governance_execute]` must be applied _before_ `#[cw_serde]`. +/// Adapted from CW++: +/// https://github.com/larry0x/cw-plus-plus/tree/main/packages/ownable +#[proc_macro_attribute] +pub fn cw_ics721_governance_execute(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote! { + enum Right { + /// Actions that can be taken to alter the proxy contract's governance like in `execute`'s entry point: + /// ```rust + /// use cosmwasm_std::{from_binary, Binary, DepsMut, Env, MessageInfo, Response, Storage}; + /// use cosmwasm_schema::cw_serde; + /// use cosmwasm_std::entry_point; + /// + /// #[cw_serde] + /// pub enum ExecuteMsg { + /// Governance(cw_ics721_governance::Action), + /// ReceiveNft(cw721::Cw721ReceiveMsg) + /// } + /// + /// #[cfg_attr(not(feature = "library"), entry_point)] + /// pub fn execute( + /// deps: DepsMut, + /// env: Env, + /// info: MessageInfo, + /// msg: ExecuteMsg, + /// ) -> Result { + /// match msg { + /// ExecuteMsg::Governance(action) => { + /// Ok(cw_ics721_governance::execute(deps, env, &info, action)?) + /// } + /// ExecuteMsg::ReceiveNft(msg) => { + /// Ok(cw_ics721_governance::execute_receive_nft(deps, info, msg)?) + /// } + /// } + /// } + /// ``` + Governance(::cw_ics721_governance::Action), + /// Cw721ReceiveMsg to be forwared to ICS721 (origin). + /// NOTE: this is NOT part of governance, since it is send by cw721 contract diretly to proxy + ReceiveNft(cw721::Cw721ReceiveMsg), + } + } + .into(), + ) +} + +/// Append governance-related query message variant(s) to an enum. +/// +/// For example, apply the `cw_ics721_governance_query` macro to the following enum: +/// +/// ```rust +/// use cosmwasm_schema::{cw_serde, QueryResponses}; +/// use cw_ics721_governance::cw_ics721_governance_query; +/// +/// #[cw_ics721_governance_query] +/// #[cw_serde] +/// #[derive(QueryResponses)] +/// enum QueryMsg { +/// #[returns(FooResponse)] +/// Foo {}, +/// #[returns(BarResponse)] +/// Bar {}, +/// } +/// ``` +/// +/// Is equivalent to: +/// +/// ```rust +/// use cosmwasm_schema::cw_serde; +/// use cw_ics721_governance::Governance; +/// +/// #[cw_serde] +/// #[derive(QueryResponses)] +/// enum ExecuteMsg { +/// #[returns(Governance)] +/// Governance {}, +/// #[returns(FooResponse)] +/// Foo {}, +/// #[returns(BarResponse)] +/// Bar {}, +/// } +/// ``` +/// +/// Note: `#[cw_ics721_governance_query]` must be applied _before_ `#[cw_serde]`. +/// Adapted from CW++: +/// https://github.com/larry0x/cw-plus-plus/tree/main/packages/ownable +#[proc_macro_attribute] +pub fn cw_ics721_governance_query(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote! { + enum Right { + /// Query the contract's governance information + #[returns(::cw_ics721_governance::Governance)] + Governance(), + } + } + .into(), + ) +} diff --git a/packages/cw-ics721-governance/src/lib.rs b/packages/cw-ics721-governance/src/lib.rs new file mode 100644 index 0000000..666d2e6 --- /dev/null +++ b/packages/cw-ics721-governance/src/lib.rs @@ -0,0 +1,418 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +use std::marker::PhantomData; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + coin, to_binary, Addr, Attribute, BankMsg, Binary, Coin, DepsMut, Empty, Env, MessageInfo, + Response, StdError, StdResult, Storage, WasmMsg, +}; +use cw721::Cw721ReceiveMsg; +use cw721_base::helpers::Cw721Contract; +use cw721_proxy::ProxyExecuteMsg; +use cw_storage_plus::Item; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// re-export the proc macros and the Expiration class +pub use cw_ics721_governance_derive::{cw_ics721_governance_execute, cw_ics721_governance_query}; +pub use cw_utils::Expiration; +use cw_utils::{may_pay, PaymentError}; + +/// Storage constant for the contract's governance +const GOVERNANCE: Item = Item::new("governance"); + +/// A governed contract may have: +/// - an optional owner, +/// - an origin (ICS721) where msgs are forwarded to, and +/// - an optional transfer fee. +/// +/// Owner: +/// - used in assert_owner(). For example execute_transfer_fee() allows only owner to change fees. +/// +/// Origin: +/// - ... +/// +/// Transfer Fee: +/// - ... +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct Governance { + owner: Option, + origin: Addr, + transfer_fee: Option, +} + +impl Governance { + pub fn into_attributes(self) -> Vec { + vec![ + Attribute::new( + "owner", + self.owner.map_or("none".to_string(), |o| o.to_string()), + ), + Attribute::new("origin", self.origin), + Attribute::new( + "transfer_fee", + self.transfer_fee + .map_or("none".to_string(), |o| o.to_string()), + ), + ] + } +} + +/// Actions that can be taken to alter the proxy contract's governance +#[cw_serde] +pub enum Action { + /// Changing owner of proxy. Once set, it can't be set to None - except via migration. + Owner(String), + /// ICS721 contract where Cw721ReceiveMsg is forwarded to. + Origin(String), + + /// Optional transfer fee, if provided it will be checked whether funds have been send on `receive_nft` and `bridge_nft` is called. + /// This means pratically only `bridge_nft` is eligible to call ics721, since `send_nft` is called by collection - and in case of base cw721 it doesn't send funds! + TransferFee(Option), + + /// Send funds from proxy to specified address. + SendFunds { to_address: String, amount: Coin }, + + /// Analogous to `cw721::Cw721ExecuteMsg::SendNft`, where NFT is transferred to ICS721 (escrow) and forwards `Cw721ReceiveMsg` to ICS721. + BridgeNft { + collection: String, + token_id: String, + msg: Binary, + }, +} + +/// Errors associated with the contract's governance +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum GovernanceError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Payment(#[from] PaymentError), + + #[error("Contract has no ownership")] + NoOwner, + + #[error("{0} is not the proxy's current owner")] + NotOwner(String), + + #[error("Incorrect payment amount: {0} != {1}")] + IncorrectPaymentAmount(Coin, Coin), + + #[error("{spender} not approved for NFT {token} in collection {collection}")] + MissingApproval { + spender: String, + collection: String, + token: String, + }, +} + +pub fn instantiate( + deps: DepsMut, + info: MessageInfo, + owner: Option, + origin: Option, + transfer_fee: Option, +) -> StdResult { + let owner = match owner { + Some(owner) => Some(deps.api.addr_validate(owner.as_str())?), + None => None, + }; + let origin = origin + .map(|a| deps.api.addr_validate(&a)) + .transpose()? + .unwrap_or(info.sender); + let governance = Governance { + owner, + transfer_fee, + origin, + }; + GOVERNANCE.save(deps.storage, &governance)?; + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attributes(governance.into_attributes())) +} + +pub fn load(storage: &dyn Storage) -> StdResult { + GOVERNANCE.load(storage) +} + +pub fn assert_owner(storage: &dyn Storage, sender: &Addr) -> Result<(), GovernanceError> { + let governance = load(storage)?; + match governance.owner { + Some(owner) => { + if sender != owner { + return Err(GovernanceError::NotOwner(sender.to_string())); + } + Ok(()) + } + None => Err(GovernanceError::NoOwner), + } +} + +pub fn execute( + deps: DepsMut, + env: Env, + info: &MessageInfo, + action: Action, +) -> Result { + match action { + Action::Owner(owner) => Ok(execute_owner(deps, &info.sender, owner)?), + Action::Origin(origin) => Ok(execute_origin(deps, &info.sender, origin)?), + Action::TransferFee(transfer_fee) => { + Ok(execute_transfer_fee(deps, &info.sender, transfer_fee)?) + } + Action::BridgeNft { + collection, + token_id, + msg, + } => Ok(execute_bridge_nft( + deps, env, info, collection, token_id, msg, + )?), + Action::SendFunds { to_address, amount } => { + Ok(execute_send_funds(deps, &info.sender, to_address, amount)?) + } + } +} + +pub fn query_governance(storage: &dyn Storage) -> StdResult { + to_binary(&load(storage)?) +} + +pub fn update_owner( + storage: &mut dyn Storage, + owner: Option, +) -> Result { + GOVERNANCE.update(storage, |governance| { + Ok(Governance { + owner, + origin: governance.origin, + transfer_fee: governance.transfer_fee, + }) + }) +} + +/// Only owner (if given) can change ownership. +pub fn execute_owner( + deps: DepsMut, + sender: &Addr, + addr: String, +) -> Result { + assert_owner(deps.storage, sender)?; + let owner = deps.api.addr_validate(&addr)?; + update_owner(deps.storage, Some(owner))?; + Ok(Response::default() + .add_attribute("method", "execute_owner") + .add_attribute("owner", addr)) +} + +pub fn update_origin( + storage: &mut dyn Storage, + origin: Addr, +) -> Result { + GOVERNANCE.update(storage, |governance| { + Ok(Governance { + owner: governance.owner, + origin, + transfer_fee: governance.transfer_fee, + }) + }) +} + +/// Only owner (if given) can change origin. +pub fn execute_origin( + deps: DepsMut, + sender: &Addr, + addr: String, +) -> Result { + assert_owner(deps.storage, sender)?; + let origin = deps.api.addr_validate(&addr)?; + update_origin(deps.storage, origin)?; + Ok(Response::default() + .add_attribute("method", "execute_origin") + .add_attribute("owner", addr)) +} + +pub fn update_transfer_fee( + storage: &mut dyn Storage, + transfer_fee: Option, +) -> Result { + GOVERNANCE.update(storage, |governance| { + Ok(Governance { + owner: governance.owner, + origin: governance.origin, + transfer_fee, + }) + }) +} + +/// Only owner (if given) can change transfer fee. +pub fn execute_transfer_fee( + deps: DepsMut, + sender: &Addr, + transfer_fee: Option, +) -> Result { + assert_owner(deps.storage, sender)?; + update_transfer_fee(deps.storage, transfer_fee.clone())?; + Ok(Response::default() + .add_attribute("method", "execute_transfer_fee") + .add_attribute( + "transfer_fee", + transfer_fee.map_or("".to_string(), |t| format!("{} {}", t.amount, t.denom)), + )) +} + +/// Check whether contract is eligible to transfer NFT from collection to itself. +pub fn check_approval( + deps: &DepsMut, + token_id: String, + collection: String, + spender: String, +) -> Result<(), GovernanceError> { + let collection_addr = deps.api.addr_validate(&collection)?; + let approval = Cw721Contract::(collection_addr, PhantomData, PhantomData) + .approval(&deps.querier, token_id.clone(), spender.clone(), None); + match approval { + Err(_) => Err(GovernanceError::MissingApproval { + spender, + collection, + token: token_id, + }), + Ok(_) => Ok(()), + } +} + +pub fn execute_send_funds( + deps: DepsMut, + sender: &Addr, + to_address: String, + amount: Coin, +) -> Result { + assert_owner(deps.storage, sender)?; + let msg = BankMsg::Send { + to_address: to_address.clone(), + amount: vec![amount.clone()], + }; + Ok(Response::default() + .add_message(msg) + .add_attribute("method", "execute_send_funds") + .add_attribute("to_address", to_address) + .add_attribute("amount", format!("{}", amount))) +} + +/// Bridging an NFT requires caller to send funds (if transfer fee is given) and does 2 things: +/// (1) a sub message to the collection is triggered for sending NFT to this contract. +/// (2) received submessage forwards msg to ICS721 for interchain transfers. +pub fn execute_bridge_nft( + deps: DepsMut, + env: Env, + info: &MessageInfo, + collection: String, + token_id: String, + msg: Binary, +) -> Result { + check_paid(deps.storage, info)?; + check_approval( + &deps, + token_id.clone(), + collection.clone(), + env.contract.address.to_string(), + )?; + + let governance = load(deps.storage)?; + let transfer_nft_msg = WasmMsg::Execute { + contract_addr: collection.to_string(), // sender is collection + msg: to_binary(&cw721::Cw721ExecuteMsg::TransferNft { + recipient: governance.origin.to_string(), + token_id: token_id.clone(), + })?, + funds: vec![], + }; + + // forward msg to ICS721, for this sender must be collection + let receive_msg = Cw721ReceiveMsg { + msg, + sender: info.sender.to_string(), + token_id: token_id.clone(), + }; + let receive_proxy_msg = WasmMsg::Execute { + contract_addr: governance.origin.to_string(), // ICS721 + msg: to_binary(&ProxyExecuteMsg::ReceiveProxyNft { + eyeball: collection.clone(), + msg: receive_msg, + })?, + funds: vec![], + }; + + Ok(Response::default() + .add_messages(vec![transfer_nft_msg, receive_proxy_msg]) + .add_attribute("method", "execute_bridge_nft") + .add_attribute("collection", collection) + .add_attribute("token_id", token_id)) +} + +/// Delegates receive msg to ICS721 contract. +/// IMPORTANT: in case transfer fee is set, info.funds must contain fee! +/// For example sending funds, using proxy's `BridgeNFT` will work. It won't work using collection's `SendNFT` message! +pub fn execute_receive_nft( + deps: DepsMut, + info: MessageInfo, + msg: Cw721ReceiveMsg, +) -> Result { + check_paid(deps.storage, &info)?; + Ok(Response::default() + .add_message(WasmMsg::Execute { + contract_addr: load(deps.storage)?.origin.into_string(), // ICS721 + msg: to_binary(&ProxyExecuteMsg::ReceiveProxyNft { + eyeball: info.sender.to_string(), + msg, + })?, + funds: vec![], + }) + .add_attribute("method", "execute_receive_nft") + .add_attribute("collection", info.sender)) +} + +/// Check whether sender has send funds, based on transfer fee (if given). +pub fn check_paid(storage: &dyn Storage, info: &MessageInfo) -> Result { + let governance = GOVERNANCE.load(storage)?; + match governance.transfer_fee { + None => Ok(true), + Some(transaction_fee) => { + let payment = may_pay(info, &transaction_fee.denom)?; + if payment != transaction_fee.amount { + return Err(GovernanceError::IncorrectPaymentAmount( + coin(payment.u128(), &transaction_fee.denom), + transaction_fee, + )); + } + Ok(true) + } + } +} + +/// Migrates the contract from the previous version to the current +/// version. +pub fn migrate( + deps: DepsMut, + owner: Option, + origin: Option, + transfer_fee: Option, +) -> StdResult { + let mut governance = load(deps.storage)?; + if let Some(origin) = origin { + governance.origin = deps.api.addr_validate(&origin)?; + } + if let Some(owner) = owner { + governance.owner = Some(deps.api.addr_validate(&owner)?); + } + governance.transfer_fee = transfer_fee; + GOVERNANCE.save(deps.storage, &governance)?; + Ok(Response::default() + .add_attribute("method", "migrate") + .add_attributes(governance.into_attributes())) +} + +#[cfg(test)] +mod tests; diff --git a/packages/cw-ics721-governance/src/tests.rs b/packages/cw-ics721-governance/src/tests.rs new file mode 100644 index 0000000..b75b9b4 --- /dev/null +++ b/packages/cw-ics721-governance/src/tests.rs @@ -0,0 +1,359 @@ +use cosmwasm_std::testing::{mock_dependencies, mock_info}; + +use super::*; + +fn mock_addresses() -> [Addr; 4] { + [ + Addr::unchecked("owner"), + Addr::unchecked("other"), + Addr::unchecked("origin"), + Addr::unchecked("ark"), + ] +} + +//------------------------------------------------------------------------------ +// Unit Tests +//------------------------------------------------------------------------------ + +#[test] +fn instantiate_origin_specified() { + let mut deps = mock_dependencies(); + let [owner, _, origin, foo] = mock_addresses(); + let transfer_fee = Some(coin(100, "uark")); + let info = mock_info(foo.as_str(), &vec![]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + Some(origin.to_string()), + transfer_fee.clone(), + ) + .unwrap(); + + // governance returned is same as governance stored. + assert_eq!( + GOVERNANCE.load(deps.as_ref().storage).unwrap(), + Governance { + owner: Some(owner), + origin, + transfer_fee, + }, + ); +} + +#[test] +fn instantiate_sender_is_origin() { + let mut deps = mock_dependencies(); + let [owner, _, _, foo] = mock_addresses(); + let transfer_fee = Some(coin(100, "uark")); + let info = mock_info(foo.as_str(), &vec![]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + None, + transfer_fee.clone(), + ) + .unwrap(); + + // governance returned is same as governance stored. + assert_eq!( + GOVERNANCE.load(deps.as_ref().storage).unwrap(), + Governance { + owner: Some(owner), + origin: foo, + transfer_fee, + }, + ); +} + +#[test] +fn instantiate_governance_no_owner() { + let mut deps = mock_dependencies(); + let [_, _, origin, foo] = mock_addresses(); + + let info = mock_info(foo.as_str(), &vec![]); + instantiate(deps.as_mut(), info, None, Some(origin.to_string()), None).unwrap(); + assert_eq!( + GOVERNANCE.load(deps.as_ref().storage).unwrap(), + Governance { + owner: None, + origin, + transfer_fee: None, + }, + ); +} + +#[test] +fn asserting_owner() { + let mut deps = mock_dependencies(); + let [owner, other, origin, foo] = mock_addresses(); + + // case 1. owner is set + { + let info = mock_info(foo.as_str(), &vec![]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + Some(origin.to_string()), + None, + ) + .unwrap(); + + // sender is owner + let res = assert_owner(deps.as_ref().storage, &owner); + assert!(res.is_ok()); + // sender is not owner + let res = assert_owner(deps.as_ref().storage, &other); + assert_eq!( + res.unwrap_err(), + GovernanceError::NotOwner(other.to_string()) + ); + } + + // case 2. owner is not set + { + update_owner(deps.as_mut().storage, None).unwrap(); + + let res = assert_owner(deps.as_ref().storage, &owner); + assert_eq!(res.unwrap_err(), GovernanceError::NoOwner); + } +} + +#[test] +fn executing_owner() { + let mut deps = mock_dependencies(); + let [owner, other, origin, foo] = mock_addresses(); + + // case 1. owner is set + { + let info = mock_info(foo.as_str(), &vec![]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + Some(origin.to_string()), + None, + ) + .unwrap(); + + // sender is owner + let res = execute_owner(deps.as_mut(), &owner, other.to_string()); + assert!(res.is_ok()); + // sender is not owner + let res = execute_owner(deps.as_mut(), &foo, owner.to_string()); + assert_eq!(res.unwrap_err(), GovernanceError::NotOwner(foo.to_string())); + } + + // case 2. owner is not set + { + // first set owner to none + update_owner(deps.as_mut().storage, None).unwrap(); + // now call againclone + let res = execute_owner(deps.as_mut(), &other, "foo".to_string()); + assert_eq!(res.unwrap_err(), GovernanceError::NoOwner); + } +} + +#[test] +fn executing_origin() { + let mut deps = mock_dependencies(); + let [owner, other, origin, foo] = mock_addresses(); + + // case 1. owner is set + { + let info = mock_info(foo.as_str(), &vec![]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + Some(origin.to_string()), + None, + ) + .unwrap(); + + // sender is owner + let res = execute_origin(deps.as_mut(), &owner, other.to_string()); + assert!(res.is_ok()); + // sender is not owner + let res = execute_origin(deps.as_mut(), &other, other.to_string()); + assert_eq!( + res.unwrap_err(), + GovernanceError::NotOwner(other.to_string()) + ); + } + + // case 2. owner is not set + { + // first set owner to none + update_owner(deps.as_mut().storage, None).unwrap(); + + let res = execute_origin(deps.as_mut(), &other, origin.to_string()); + assert_eq!(res.unwrap_err(), GovernanceError::NoOwner); + } +} + +#[test] +fn executing_transfer_fee() { + let mut deps = mock_dependencies(); + let [owner, other, origin, foo] = mock_addresses(); + let transfer_fee = Some(coin(100, "uark")); + + // case 1. owner is set + { + let info = mock_info(foo.as_str(), &vec![]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + Some(origin.to_string()), + None, + ) + .unwrap(); + + // sender is owner + let res = execute_transfer_fee(deps.as_mut(), &owner, transfer_fee.clone()); + assert!(res.is_ok()); + // sender is not owner + let res = execute_transfer_fee(deps.as_mut(), &other, transfer_fee.clone()); + assert_eq!( + res.unwrap_err(), + GovernanceError::NotOwner(other.to_string()) + ); + } + + // case 2. owner is not set + { + // first set owner to none + update_owner(deps.as_mut().storage, None).unwrap(); + + let res = execute_transfer_fee(deps.as_mut(), &other, transfer_fee); + assert_eq!(res.unwrap_err(), GovernanceError::NoOwner); + } +} + +#[test] +fn executing_send_funds() { + let mut deps = mock_dependencies(); + let [owner, other, origin, foo] = mock_addresses(); + let funds = coin(100, "uark"); + + // case 1. owner is set + { + let info = mock_info(foo.as_str(), &vec![]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + Some(origin.to_string()), + None, + ) + .unwrap(); + + // sender is owner + let res = execute_send_funds(deps.as_mut(), &owner, foo.to_string(), funds.clone()); + assert!(res.is_ok()); + // sender is not owner + let res = execute_send_funds(deps.as_mut(), &other, foo.to_string(), funds.clone()); + assert_eq!( + res.unwrap_err(), + GovernanceError::NotOwner(other.to_string()) + ); + } + + // case 2. owner is not set + { + // first set owner to none + update_owner(deps.as_mut().storage, None).unwrap(); + + let res = execute_send_funds(deps.as_mut(), &other, foo.to_string(), funds); + assert_eq!(res.unwrap_err(), GovernanceError::NoOwner); + } +} + +#[test] +fn checking_paid() { + let mut deps = mock_dependencies(); + let [owner, _, origin, foo] = mock_addresses(); + let funds = coin(100000, "uark"); + let info = mock_info(foo.as_str(), &vec![funds.clone()]); + instantiate( + deps.as_mut(), + info, + Some(owner.to_string()), + Some(origin.to_string()), + None, + ) + .unwrap(); + + // case 1. no transfer fee + { + // sender hasn't send funds + let info = mock_info(foo.as_str(), &vec![]); + let res = check_paid(&deps.storage, &info); + assert!(res.is_ok()); + + // sender can send transfer fee, even if none is defined + let funds = coin(100000, "uark"); + let info = mock_info(foo.as_str(), &vec![funds.clone()]); + let res = check_paid(&deps.storage, &info); + assert!(res.is_ok()); + } + + // case 2. transfer fee is set + { + // set transfer fee + let transfer_fee = coin(100, "uark"); + execute_transfer_fee(deps.as_mut(), &owner, Some(transfer_fee.clone())).unwrap(); + + // sender hasn't send funds + let info = mock_info(foo.as_str(), &vec![]); + let res = check_paid(&deps.storage, &info); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err(), + GovernanceError::IncorrectPaymentAmount(coin(0, "uark"), transfer_fee.clone()) + ); + + // sender send transfer fee + let funds = coin(100, "uark"); + let info = mock_info(foo.as_str(), &vec![funds.clone()]); + let res = check_paid(&deps.storage, &info); + assert!(res.is_ok()); + + // sender send less than transfer fee + let funds = coin(50, "uark"); + let info = mock_info(foo.as_str(), &vec![funds.clone()]); + let res = check_paid(&deps.storage, &info); + assert_eq!( + res.unwrap_err(), + GovernanceError::IncorrectPaymentAmount(funds, transfer_fee.clone()) + ); + + // sender send more than transfer fee + let funds = coin(150, "uark"); + let info = mock_info(foo.as_str(), &vec![funds.clone()]); + let res = check_paid(&deps.storage, &info); + assert_eq!( + res.unwrap_err(), + GovernanceError::IncorrectPaymentAmount(funds, transfer_fee.clone()) + ); + } +} +#[test] +fn into_attributes_works() { + assert_eq!( + Governance { + owner: Some(Addr::unchecked("ark")), + origin: Addr::unchecked("protocol"), + transfer_fee: None, + } + .into_attributes(), + vec![ + Attribute::new("owner", "ark"), + Attribute::new("origin", "protocol"), + Attribute::new("transfer_fee", "none") + ], + ); +} diff --git a/packages/cw-ics721-governance/tests/macro.rs b/packages/cw-ics721-governance/tests/macro.rs new file mode 100644 index 0000000..72ed573 --- /dev/null +++ b/packages/cw-ics721-governance/tests/macro.rs @@ -0,0 +1,59 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_ics721_governance::{cw_ics721_governance_execute, cw_ics721_governance_query, Action}; + +#[cw_ics721_governance_execute] +#[cw_serde] +enum ExecuteMsg { + Foo, + Bar(u64), + Fuzz { buzz: String }, +} + +#[cw_ics721_governance_query] +#[cw_serde] +#[derive(QueryResponses)] +enum QueryMsg { + #[returns(String)] + Foo, + + #[returns(String)] + Bar(u64), + + #[returns(String)] + Fuzz { buzz: String }, +} + +#[test] +fn derive_execute_variants() { + let msg = ExecuteMsg::Foo; + + // If this compiles we have won. + match msg { + ExecuteMsg::Governance(Action::Owner { .. }) + | ExecuteMsg::Governance(Action::Origin(..)) + | ExecuteMsg::Governance(Action::TransferFee(..)) + | ExecuteMsg::Governance(Action::SendFunds { + to_address: _, + amount: _, + }) + | ExecuteMsg::Governance(Action::BridgeNft { + collection: _, + token_id: _, + msg: _, + }) + | ExecuteMsg::ReceiveNft(..) + | ExecuteMsg::Foo + | ExecuteMsg::Bar(_) + | ExecuteMsg::Fuzz { .. } => "yay", + }; +} + +#[test] +fn derive_query_variants() { + let msg = QueryMsg::Foo; + + // If this compiles we have won. + match msg { + QueryMsg::Governance() | QueryMsg::Foo | QueryMsg::Bar(_) | QueryMsg::Fuzz { .. } => "yay", + }; +} diff --git a/packages/cw-rate-limiter/Cargo.toml b/packages/cw-rate-limiter/Cargo.toml index 6b92ed8..53d4a1b 100644 --- a/packages/cw-rate-limiter/Cargo.toml +++ b/packages/cw-rate-limiter/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" version = "0.0.1" [dependencies] -cosmwasm-std = "1.1" -cosmwasm-schema = "1.1" -cw-storage-plus = "0.16" -thiserror = "1" -schemars = "0.8.11" -serde = "1.0" +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } diff --git a/packages/cw721-proxy-derive/Cargo.toml b/packages/cw721-proxy-derive/Cargo.toml index 48857ce..86bfade 100644 --- a/packages/cw721-proxy-derive/Cargo.toml +++ b/packages/cw721-proxy-derive/Cargo.toml @@ -9,7 +9,7 @@ version = "0.0.1" proc-macro = true [dependencies] -syn = { version = "1.0", features = ["derive"] } -quote = "1.0" -proc-macro2 = "1.0" -cw721 = "0.16" \ No newline at end of file +cw721 = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } diff --git a/packages/cw721-proxy-multi-test/.gitignore b/packages/cw721-proxy-multi-test/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/packages/cw721-proxy-multi-test/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/packages/cw721-proxy-multi-test/Cargo.toml b/packages/cw721-proxy-multi-test/Cargo.toml new file mode 100644 index 0000000..dc6918e --- /dev/null +++ b/packages/cw721-proxy-multi-test/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cw721-proxy-multi-test" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-multi-test = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true } +cw721-proxy-tester = { workspace = true } +ibc-outgoing-msg = { workspace = true } +anyhow = { workspace = true } +cw-utils = { workspace = true } +cw-ics721-governance = { workspace = true } diff --git a/packages/cw721-proxy-multi-test/src/lib.rs b/packages/cw721-proxy-multi-test/src/lib.rs new file mode 100644 index 0000000..ba8bbc8 --- /dev/null +++ b/packages/cw721-proxy-multi-test/src/lib.rs @@ -0,0 +1,274 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty, IbcTimeout, IbcTimeoutBlock, StdResult}; +use cw721_base::ExecuteMsg as Cw721ExecuteMsg; +use cw_ics721_governance::{cw_ics721_governance_execute, Action}; +use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; +use ibc_outgoing_msg::IbcOutgoingMsg; + +#[cw_ics721_governance_execute] +#[cw_serde] +pub enum ExecuteMsg {} + +pub struct Test { + pub app: App, + #[allow(dead_code)] + pub cw721_id: u64, + pub cw721s: Vec, + pub minter: Addr, + pub other: Addr, + + pub mock_receiver: Addr, + pub transfer_fee: Option, + + pub nfts_minted: usize, +} + +impl Test { + pub fn new(cw721s: usize, transfer_fee: Option) -> Self { + let minter = Addr::unchecked("minter"); + let other = Addr::unchecked("other"); + let mut app = App::new(|router, _, storage| { + router + .bank + .init_balance(storage, &minter, vec![coin(10000, "uark")]) + .unwrap(); + router + .bank + .init_balance(storage, &other, vec![coin(10000, "uark")]) + .unwrap(); + }); + + let cw721_id = app.store_code(cw721_base()); + let proxy_tester_code_id = app.store_code(cw721_proxy_tester()); + + let mock_receiver = app + .instantiate_contract( + proxy_tester_code_id, + minter.clone(), + &cw721_proxy_tester::msg::InstantiateMsg::default(), + &[], + "proxy_tester", + None, + ) + .unwrap(); + + let cw721_instantiate_msg = |id: usize| cw721_base::msg::InstantiateMsg { + name: format!("cw721 {}", id), + symbol: format!("{}", id), + minter: minter.to_string(), + }; + let cw721s: Vec<_> = (0..cw721s) + .map(|id| { + app.instantiate_contract( + cw721_id, + minter.clone(), + &cw721_instantiate_msg(id), + &[], + format!("cw721 {}", id), + None, + ) + .unwrap() + }) + .collect(); + + Self { + app, + cw721_id, + cw721s, + minter, + other, + mock_receiver, + transfer_fee, + nfts_minted: 0, + } + } + + pub fn mint(&mut self, collection: Addr) -> Result { + self.nfts_minted += 1; + + self.app.execute_contract( + self.minter.clone(), + collection, + &cw721_base::msg::ExecuteMsg::::Mint(cw721_base::MintMsg:: { + token_id: self.nfts_minted.to_string(), + owner: self.minter.to_string(), + token_uri: None, + extension: Default::default(), + }), + &[], + )?; + // return token id + Ok(self.nfts_minted.to_string()) + } + + pub fn ibc_outgoing_msg(&self, channel_id: String) -> IbcOutgoingMsg { + IbcOutgoingMsg { + channel_id, + memo: None, + receiver: "dummy".to_string(), + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 10, + }), + } + } + + pub fn approve( + &mut self, + collection: Addr, + token_id: String, + spender: String, + ) -> Result { + let approve_msg: Cw721ExecuteMsg = Cw721ExecuteMsg::Approve { + spender, + token_id, + expires: Some(cw_utils::Expiration::Never {}), + }; + let res = self + .app + .execute_contract(self.minter.clone(), collection, &approve_msg, &[])?; + + Ok(res) + } + + pub fn bridge_nft( + &mut self, + sender: Addr, + proxy: Addr, + collection: Addr, + token_id: String, + channel_id: String, + transfer_fee: Option, + ) -> Result { + let funds = transfer_fee.map_or(vec![], |fee| vec![fee]); + let res = self.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::BridgeNft { + collection: collection.to_string(), + token_id, + msg: to_binary(&self.ibc_outgoing_msg(channel_id))?, + }), + &funds, + )?; + + Ok(res) + } + + pub fn send_nft( + &mut self, + sender: Addr, + contract: String, // target + collection: Addr, // source + token_id: String, + channel_id: String, + transfer_fee: Option, + ) -> Result { + let funds = transfer_fee.map_or(vec![], |fee| vec![fee]); + let res = self.app.execute_contract( + sender, + collection, + &cw721_base::msg::ExecuteMsg::::SendNft { + contract, + token_id, + msg: to_binary(&self.ibc_outgoing_msg(channel_id))?, + }, + &funds, + )?; + Ok(res) + } + + pub fn query_last_msg(&self) -> StdResult { + // in case proxy passed message to origin + self.app.wrap().query_wasm_smart( + &self.mock_receiver, + &cw721_proxy_tester::msg::QueryMsg::LastMsg {}, + ) + } + + pub fn execute_owner( + &mut self, + sender: Addr, + proxy: Addr, + addr: Addr, + ) -> Result { + let res = self.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::Owner(addr.to_string())), + &[], + )?; + + Ok(res) + } + + pub fn execute_origin( + &mut self, + sender: Addr, + proxy: Addr, + addr: Addr, + ) -> Result { + let res = self.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::Origin(addr.to_string())), + &[], + )?; + + Ok(res) + } + + pub fn send_funds( + &mut self, + sender: Addr, + proxy: Addr, + to_address: String, + amount: Coin, + ) -> Result { + let res = self.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::SendFunds { + to_address, + amount: amount.clone(), + }), + &[amount], + )?; + + Ok(res) + } + + pub fn execute_transfer_fee( + &mut self, + sender: Addr, + proxy: Addr, + transfer_fee: Option, + ) -> Result { + let res = self.app.execute_contract( + sender, + proxy, + &ExecuteMsg::Governance(Action::TransferFee(transfer_fee)), + &[], + )?; + + Ok(res) + } +} + +pub fn cw721_base() -> Box> { + let contract = ContractWrapper::new( + cw721_base::entry::execute, + cw721_base::entry::instantiate, + cw721_base::entry::query, + ); + Box::new(contract) +} + +pub fn cw721_proxy_tester() -> Box> { + let contract = ContractWrapper::new( + cw721_proxy_tester::contract::execute, + cw721_proxy_tester::contract::instantiate, + cw721_proxy_tester::contract::query, + ); + Box::new(contract) +} diff --git a/packages/cw721-proxy/Cargo.toml b/packages/cw721-proxy/Cargo.toml index 6f30517..66b7814 100644 --- a/packages/cw721-proxy/Cargo.toml +++ b/packages/cw721-proxy/Cargo.toml @@ -6,12 +6,9 @@ edition = "2021" version = "0.0.1" [dependencies] -cosmwasm-std = "1.1.1" -cosmwasm-schema = "1.1.1" - -cw-storage-plus = "0.16" -cw721 = "0.16" - -thiserror = { version = "1.0.31" } - -cw721-proxy-derive = { path = "../cw721-proxy-derive", version = "*" } \ No newline at end of file +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw721 = { workspace = true } +thiserror = { workspace = true } +cw721-proxy-derive = { workspace = true } \ No newline at end of file diff --git a/packages/cw721-whitelist-map/.gitignore b/packages/cw721-whitelist-map/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/packages/cw721-whitelist-map/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/packages/cw721-whitelist-map/Cargo.toml b/packages/cw721-whitelist-map/Cargo.toml new file mode 100644 index 0000000..f8ee834 --- /dev/null +++ b/packages/cw721-whitelist-map/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cw721-whitelist-map" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } diff --git a/packages/cw721-whitelist-map/src/lib.rs b/packages/cw721-whitelist-map/src/lib.rs new file mode 100644 index 0000000..51e27bb --- /dev/null +++ b/packages/cw721-whitelist-map/src/lib.rs @@ -0,0 +1,59 @@ +use cosmwasm_schema::serde::{de::DeserializeOwned, Serialize}; +use cosmwasm_std::{StdResult, Storage}; +use cw_storage_plus::{KeyDeserialize, Map, PrimaryKey}; + +pub struct WhiteListMap<'a, K, T> { + pub map: Map<'a, K, T>, +} + +impl<'a, K, T> WhiteListMap<'a, K, T> +where + K: PrimaryKey<'a> + KeyDeserialize, + T: Serialize + DeserializeOwned, +{ + pub const fn new() -> Self { + Self { + map: Map::new("whitelist"), + } + } + + pub fn has(&self, storage: &dyn Storage, key: K) -> bool { + self.map.has(storage, key) + } + + pub fn load(&self, storage: &dyn Storage, key: K) -> StdResult { + self.map.load(storage, key) + } + + pub fn may_load(&self, storage: &dyn Storage, key: K) -> StdResult> { + self.map.may_load(storage, key) + } + + pub fn query_is_whitelisted

( + &self, + storage: &dyn Storage, + key: K, + mut predicate: P, + ) -> StdResult + where + P: FnMut(T) -> bool, + { + match self.may_load(storage, key)? { + Some(value) => Ok(predicate(value)), + None => Ok(false), + } + } + + pub fn save(&self, storage: &mut dyn Storage, key: K, value: &T) -> StdResult<()> { + self.map.save(storage, key, value) + } + + pub fn remove(&self, storage: &mut dyn Storage, key: K) { + self.map.remove(storage, key); + } + + pub fn clear(&self, storage: &mut dyn Storage) -> StdResult<()> { + self.map.clear(storage); + Ok(()) + } +} diff --git a/packages/cw721-whitelist/.gitignore b/packages/cw721-whitelist/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/packages/cw721-whitelist/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/packages/cw721-whitelist/Cargo.toml b/packages/cw721-whitelist/Cargo.toml new file mode 100644 index 0000000..68a5ca5 --- /dev/null +++ b/packages/cw721-whitelist/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cw721-whitelist" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } diff --git a/packages/cw721-whitelist/src/lib.rs b/packages/cw721-whitelist/src/lib.rs new file mode 100644 index 0000000..451b1fa --- /dev/null +++ b/packages/cw721-whitelist/src/lib.rs @@ -0,0 +1,64 @@ +use cosmwasm_schema::serde::{de::DeserializeOwned, Serialize}; +use cosmwasm_std::{StdResult, Storage}; +use cw_storage_plus::Item; + +pub struct Whitelist<'a, T> { + whitelist: Item<'a, Vec>, +} + +impl<'a, T> Whitelist<'a, T> +where + T: Serialize + DeserializeOwned + PartialEq + Clone, +{ + pub const fn new() -> Self { + Self { + whitelist: Item::new("whitelist"), + } + } + + pub fn init(&self, storage: &mut dyn Storage, whitelist: &Option>) -> StdResult<()> { + let whitelist = whitelist.clone().map_or(vec![], |wl| wl); + self.whitelist.save(storage, &whitelist) + } + + pub fn query_whitelist(&self, storage: &dyn Storage) -> StdResult> { + match self.whitelist.may_load(storage).unwrap_or(None) { + Some(e) => Ok(e), + None => Ok(vec![]), + } + } + + pub fn query_is_whitelisted(&self, storage: &dyn Storage, value: &T) -> StdResult { + let whitelist = self.query_whitelist(storage)?; + Ok(whitelist.contains(value)) + } + + pub fn add(&self, storage: &mut dyn Storage, value: &T) -> StdResult<()> { + let mut whitelist = self.query_whitelist(storage)?; + match whitelist.contains(value) { + true => Ok(()), + false => { + whitelist.push(value.clone()); + self.whitelist.save(storage, &whitelist)?; + Ok(()) + } + } + } + + pub fn remove(&self, storage: &mut dyn Storage, value: &T) -> StdResult<()> { + let mut whitelist = self.query_whitelist(storage)?; + match whitelist.contains(value) { + true => { + whitelist.remove(whitelist.iter().position(|x| x.eq(value)).unwrap()); + self.whitelist.save(storage, &whitelist)?; + Ok(()) + } + false => Ok(()), + } + } + + pub fn clear(&self, storage: &mut dyn Storage) -> StdResult<()> { + self.whitelist.save(storage, &Vec::new())?; + Ok(()) + } +} diff --git a/packages/ibc-outgoing-msg/.gitignore b/packages/ibc-outgoing-msg/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/packages/ibc-outgoing-msg/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/packages/ibc-outgoing-msg/Cargo.toml b/packages/ibc-outgoing-msg/Cargo.toml new file mode 100644 index 0000000..c7a00f8 --- /dev/null +++ b/packages/ibc-outgoing-msg/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ibc-outgoing-msg" +version = "0.0.1" +description = "TODO" +license = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +keywords = { workspace = true } + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } diff --git a/packages/ibc-outgoing-msg/src/lib.rs b/packages/ibc-outgoing-msg/src/lib.rs new file mode 100644 index 0000000..9ca6488 --- /dev/null +++ b/packages/ibc-outgoing-msg/src/lib.rs @@ -0,0 +1,17 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::IbcTimeout; + +/// Copied from: https://github.com/public-awesome/ics721/blob/main/contracts/cw-ics721-bridge/src/msg.rs#L84-L95 +#[cw_serde] +pub struct IbcOutgoingMsg { + /// The address that should receive the NFT being sent on the + /// *receiving chain*. + pub receiver: String, + /// The *local* channel ID this ought to be sent away on. This + /// contract must have a connection on this channel. + pub channel_id: String, + /// Timeout for the IBC message. + pub timeout: IbcTimeout, + /// Memo to add custom string to the msg + pub memo: Option, +}