From 88ae9fde1079c45f3208da20c0a106521e86e87e Mon Sep 17 00:00:00 2001 From: Matthias <97468149+matthiasmatt@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:32:33 +0100 Subject: [PATCH 1/5] doc: Add cookbook for live smart contracts (#109) --- Cookbook.md | 581 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 Cookbook.md diff --git a/Cookbook.md b/Cookbook.md new file mode 100644 index 0000000..d1a5d7f --- /dev/null +++ b/Cookbook.md @@ -0,0 +1,581 @@ +# Contracts Cookbook + +This file describes the different messages that can be sent as queries or transactions +to the contracts of this repository with a description of the expected behavior. + +## 1. Core cw3 flex multisig + +This contract is a multisig contract that is backed by a cw4 (group) contract, which independently maintains the voter set. + +### 1.1 Instantiate + +```javascript +{ + "group_addr": "cosmos1...", // this is the group contract that contains the member list + "threshold": { + "absolute_count": {"weight": 2}, + "absolute_percentage": {"percentage": 0.5}, + "threshold_quorum": { "threshold": 0.1, "quorum": 0.2 } + }, + "max_voting_period": "3600s", + // who is able to execute passed proposals + // None means that anyone can execute + "executor": {}, + /// The cost of creating a proposal (if any). + "proposal_deposit": { + "denom": "uusd", + "amount": "1000000" + }, +} +``` + +### 1.2 Execute + +- **Propose** creates a message to be executed by the multisig. It can be executed by anyone. + +```javascript +{ + "propose": { + "title": "My proposal", + "description": "This is a proposal", + "msgs": [ + { + "bank": { + "send": { + "from_address": "cosmos1...", + "to_address": "cosmos1...", + "amount": [{ "denom": "uusd", "amount": "1000000" }] + } + } + } + ], + "latest": { + "at_height": 123456 + } + } +} +``` + +- **Vote** adds a vote to an existing proposal. It can be executed by anyone. + +```javascript +{ + "vote": { + "proposal_id": 1, + "vote": "yes" + } +} +``` + +- **Execute** executes a passed proposal. It can be executed by anyone. + +```javascript +{ + "execute": { + "proposal_id": 1 + } +} +``` + +- **Close** closes an expired proposal. It can be executed by anyone. + +```javascript +{ + "close": { + "proposal_id": 1 + } +} +``` + +### 1.3 Query + +- **Threshold** returns the current threshold necessary for a proposal to be executed. + +```javascript +{ + "threshold": {} +} +``` + +- **Proposal** fetches the details of a specific proposal given its ID. + +```javascript +{ + "proposal": { + "proposal_id": 1 + } +} +``` + +- **ListProposals** lists proposals with optional pagination. `start_after` specifies the ID after which to start listing, and `limit` sets the maximum number of proposals to return. + +```javascript +{ + "list_proposals": { + "start_after": 1, + "limit": 10 + } +} +``` + +- **ReverseProposals** lists proposals in reverse order with optional pagination. `start_before` specifies the ID before which to start listing in reverse, and `limit` sets the maximum number of proposals to return. + +```javascript +{ + "reverse_proposals": { + "start_before": 10, + "limit": 10 + } +} +``` + +- **Vote** retrieves the vote details for a given proposal ID and voter address. + +```javascript +{ + "vote": { + "proposal_id": 1, + "voter": "cosmos1..." + } +} +``` + +- **ListVotes** lists votes for a given proposal, with optional pagination. `start_after` specifies the address after which to start listing votes, and `limit` sets the maximum number of votes to return. + +```javascript +{ + "list_votes": { + "proposal_id": 1, + "start_after": "cosmos1...", + "limit": 10 + } +} +``` + +- **Voter** fetches details about a specific voter by their address. + +```javascript +{ + "voter": { + "address": "cosmos1..." + } +} +``` + +- **ListVoters** lists voters with optional pagination. `start_after` specifies the address after which to start listing voters, and `limit` sets the maximum number of voters to return. + +```javascript +{ + "list_voters": { + "start_after": "cosmos1...", + "limit": 10 + } +} +``` + +- **Config** retrieves the current configuration of the system. + +```javascript +{ + "config": {} +} +``` + +## 2. Core shifter + +Shifter is a simple contract that can execute peg and depth shift to any markets in the x/perp module of Nibiru. +The contract holds a whitelist of addressses that are allowed to execute the shift. + +### 2.1 Instantiate + +The instantiation defines just the onwer of the contract, who wil be able to add and remove addresses from the whitelist, and execute the shifts. + +```javascript +{"owner": "cosmos1..."} +``` + +### 2.2 Execute + +- **ShiftSwapInvariant** executes a depth shift in a market. + +```javascript +{ + "shift_swap_invariant": { + "pair": "uusd:usdr", + "new_swap_invariant": "1000000" + } +} +``` + +- **ShiftPegMultiplier** executes a depth shift on a market. It can be executed by anyone. + +```javascript +{ + "shift_peg_multiplier": { + "pair": "ubtc:unusd", + "new_peg_mult": "20420.69" + } +} +``` + +- **EditOpers** adds or removes addresses from the whitelist. It can be executed by the owner. + +```javascript +{ + "edit_opers": { + "add_oper": {"addr": "cosmos1..."}, + "remove_oper": {"addr": "cosmos1..."}, + } +} +``` + +### 2.3 Query + +The queries have to do with checking permissions of addresses. + +- **HasPerms** checks if an address has permissions to execute shifts. + +```javascript +{ + "has_perms": { + "address": "cosmos1..." + } +} +``` + +- **Perms** query the contract owner and set of operators. + +```javascript +{ + "perms": {}, +} +``` + +## 3. Core token vesting + +This contract implements vesting accounts for the CW20 and native tokens. + +### 3.1 Instantiate + +There's no instantiation message. + +```javascript +{} +``` + +### 3.2 Execute + +- **Receive** + +```javascript +{ + "receive": { + "sender": "cosmos1...", + "amount": "1000000", + "msg": "eyJ2ZXN0X2lkIjoxLCJ2ZXN0X3R5cGUiOiJ2ZXN0In0=", + } +} +``` + +- **RegisterVestingAccount** registers a vesting account + +```javascript +{ + "register_vesting_account": { + "address": "cosmos1...", + "master_address": "cosmos1...", + "vesting_schedule": { + "linear_vesting": { + "start_time": "1703772805", + "end_time": "1703872805", + "vesting_amount": "1000000" + } + } + } +} +``` + +- **DeregisterVestingAccount** deregisters a vesting account + +```javascript +{ + "deregister_vesting_account": { + "address": "cosmos1...", + "denom": "uusd", + "vested_token_recipient": "cosmos1...", // address that will receive the vested tokens after deregistration. If None, tokens are received by the owner address. + "left_vested_token_recipient": "cosmos1...", // address that will receive the left vesting tokens after deregistration. + } +} +``` + +- **Claim** allows to claim vested tokens + +```javascript +{ + "claim": { + "denom": "uusd", + "recipient": "cosmos1...", + } +} +``` + +### 3.3 Query + +- **VestingAccount** returns the vesting account details for a given address. + +```javascript +{ + "vesting_account": { + "address": "cosmos1...", + } +} +``` + +## 4. Nibi Stargate + +This smart contract showcases usage examples for certain Nibiru-specific and Cosmos-SDK-specific. + +### 4.1 Instantiate + +There's no instantiation message. + +```javascript +{} +``` + +### 4.2 Execute + +- **CreateDenom** creates a new denom + +```javascript +{ + "create_denom": { "subdenom": "zzz" } +} +``` + +- **Mint** mints tokens + +```javascript +{ + "mint": { + "coin": { "amount": "[amount]", "denom": "tf/[contract-addr]/[subdenom]" }, + "mint_to": "[mint-to-addr]" + } +} +``` + +- **Burn** burns tokens + +```javascript +{ + "burn": { + "coin": { "amount": "[amount]", "denom": "tf/[contract-addr]/[subdenom]" }, + "burn_from": "[burn-from-addr]" + } +} +``` + +- **ChangeAdmin** changes the admin of a denom + +```javascript +{ + "change_admin": { + "denom": "tf/[contract-addr]/[subdenom]", + "new_admin": "[ADDR]" + } +} +``` + +## 5. Nibi Stargate Perp + +This smart contract showcases usage examples for certain Nibiru-specific for the perp market. + +### 5.1 Instantiate + +The instantiation defines the owner of the contract, who will be able to add and remove addresses from the whitelist, and execute the shifts. + +```javascript +{ + "admin": "cosmos1...", +} +``` +### 5.2 Execute + +- **MarketOrder** places a market order for a specified trading pair. `pair` indicates the trading pair, `is_long` determines if it's a long or short order, `quote_amount` is the amount in the quote currency, `leverage` specifies the leverage to apply, and `base_amount_limit` sets a limit for the amount in the base currency. + +```javascript +{ + "market_order": { + "pair": "BTC/USDT", + "is_long": true, + "quote_amount": "1000000", + "leverage": "2.0", + "base_amount_limit": "5000000" + } +} +``` + +- **ClosePosition** closes an open position for a specified trading pair. + +```javascript +{ + "close_position": { + "pair": "BTC/USDT" + } +} +``` + +- **AddMargin** adds margin to an existing position for a specified trading pair. `margin` is the amount of additional margin to add. + +```javascript +{ + "add_margin": { + "pair": "BTC/USDT", + "margin": {"denom": "usdt", "amount": "100000"} + } +} +``` + +- **RemoveMargin** removes margin from an existing position for a specified trading pair. `margin` is the amount of margin to remove. + +```javascript +{ + "remove_margin": { + "pair": "BTC/USDT", + "margin": {"denom": "usdt", "amount": "50000"} + } +} +``` + +- **MultiLiquidate** triggers multiple liquidations based on the provided arguments. `liquidations` is a list of liquidation arguments specifying the details for each liquidation. + +```javascript +{ + "multi_liquidate": { + "liquidations": [ + { + "pair": "BTC/USDT", + "trader": "cosmos1...", + }, + { + "pair": "BTC/USDT", + "trader": "cosmos1...", + } + ] + } +} +``` + +- **DonateToInsuranceFund** allows donation to the insurance fund. `donation` is the coin and amount to donate. + +```javascript +{ + "donate_to_insurance_fund": { + "donation": {"denom": "usdt", "amount": "100000"} + } +} +``` + +- **Claim** facilitates the claiming of funds. `funds` is an optional field specifying a particular coin and amount to claim, `claim_all` is an optional flag to claim all funds, and `to` is the address to which the funds will be sent. + +```javascript +{ + "claim": { + "funds": {"denom": "usdt", "amount": "100000"}, + "claim_all": true, + "to": "cosmos1..." + } +} +``` + +This format aligns with the style of your previous documentation, ensuring consistency and clarity in the explanation of each function and its parameters. + +## 6. Nusd Valuator + +This smart contract is a simple valuator for the nusd token, which takes one collateral. + +### 6.1 Instantiate + +The owner is the only one who can execute messages in the contract + +```javascript +{ + "owner": "cosmos1...", + "accepted_denoms": "uusdc", +} +``` + +### 6.2 Execute + +- **ChangeDenom** updates the accepted denoms + +```javascript +{ + "change_denom": { + "from: "uusdc", + "to": "uusd", + } +} +``` + +- **AddDenom** adds a new accepted denom + +```javascript +{ + "add_denom": { + "denom": "uusd", + } +} +``` + +- **RemoveDenom** removes an accepted denom + +```javascript +{ + "remove_denom": { + "denom": "uusd", + } +} +``` + +### 6.3 Query + + +- **Mintable** queries the amount of μNUSD that can be minted in exchange for the specified set of `from_coins`. + +```javascript +{ + "mintable": { + "from_coins": ["BTC", "ETH"] + } +} +``` + +- **Redeemable** calculates the amount of a specified `to_denom` currency that is redeemable for a given `redeem_amount` of μNUSD. + +```javascript +{ + "redeemable": { + "redeem_amount": "1000000", + "to_denom": "usdt" + } +} +``` + +- **AcceptedDenoms** retrieves the set of token denominations that are accepted as collateral. + +```javascript +{ + "accepted_denoms": {} +} +``` + +- **RedeemableChoices** provides a set of possible redeemable coin options that could be received when redeeming a specified `redeem_amount` of μNUSD. + +```javascript +{ + "redeemable_choices": { + "redeem_amount": "1000000" + } +} +``` From bd792e403033125c048ce1ad6e21d630b1b6603e Mon Sep 17 00:00:00 2001 From: Kevin Yang <5478483+k-yang@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:22:23 -0800 Subject: [PATCH 2/5] feat: initial airdrop contract (#110) * feat: initial airdrop contract * create cargo config * test: instantiate message * remove owner from instantiate msg * test: query functions * test: execute functions * feat: add manager list * feat: implement manager list * Create README.md * Update contracts/airdrop/src/contract.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update contracts/airdrop/src/contract.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update contracts/airdrop/src/contract.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update contracts/airdrop/src/contract.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix error messages --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .gitignore | 4 +- Cargo.lock | 121 +++++---- Cargo.toml | 2 +- contracts/airdrop/.cargo/config | 4 + contracts/airdrop/Cargo.toml | 55 ++++ contracts/airdrop/README.md | 13 + contracts/airdrop/src/contract.rs | 256 ++++++++++++++++++ contracts/airdrop/src/lib.rs | 6 + contracts/airdrop/src/msg.rs | 43 +++ contracts/airdrop/src/state.rs | 18 ++ contracts/airdrop/src/tests/execute/claim.rs | 79 ++++++ contracts/airdrop/src/tests/execute/mod.rs | 3 + .../airdrop/src/tests/execute/reward_users.rs | 173 ++++++++++++ .../airdrop/src/tests/execute/withdraw.rs | 106 ++++++++ contracts/airdrop/src/tests/instantiate.rs | 66 +++++ contracts/airdrop/src/tests/mod.rs | 3 + contracts/airdrop/src/tests/query/campaign.rs | 48 ++++ contracts/airdrop/src/tests/query/mod.rs | 2 + .../airdrop/src/tests/query/user_pool.rs | 64 +++++ 19 files changed, 1007 insertions(+), 59 deletions(-) create mode 100644 contracts/airdrop/.cargo/config create mode 100644 contracts/airdrop/Cargo.toml create mode 100644 contracts/airdrop/README.md create mode 100644 contracts/airdrop/src/contract.rs create mode 100644 contracts/airdrop/src/lib.rs create mode 100644 contracts/airdrop/src/msg.rs create mode 100644 contracts/airdrop/src/state.rs create mode 100644 contracts/airdrop/src/tests/execute/claim.rs create mode 100644 contracts/airdrop/src/tests/execute/mod.rs create mode 100644 contracts/airdrop/src/tests/execute/reward_users.rs create mode 100644 contracts/airdrop/src/tests/execute/withdraw.rs create mode 100644 contracts/airdrop/src/tests/instantiate.rs create mode 100644 contracts/airdrop/src/tests/mod.rs create mode 100644 contracts/airdrop/src/tests/query/campaign.rs create mode 100644 contracts/airdrop/src/tests/query/mod.rs create mode 100644 contracts/airdrop/src/tests/query/user_pool.rs diff --git a/.gitignore b/.gitignore index 75238c5..aad66ca 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ yarn.lock contracts/*/.editorconfig packages/*/.editorconfig -lcov.info \ No newline at end of file +lcov.info + +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 98f371b..b0afd7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,23 @@ dependencies = [ "memchr", ] +[[package]] +name = "airdrop" +version = "0.0.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-storage-plus", + "cw2", + "nibiru-std", + "schemars", + "semver", + "serde", + "thiserror", +] + [[package]] name = "anstream" version = "0.6.5" @@ -87,9 +104,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" dependencies = [ "backtrace", ] @@ -316,7 +333,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -593,21 +610,20 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset 0.9.0", ] [[package]] name = "crossbeam-queue" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" dependencies = [ "cfg-if", "crossbeam-utils", @@ -615,9 +631,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" dependencies = [ "cfg-if", ] @@ -921,7 +937,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -932,7 +948,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -1135,7 +1151,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -1220,42 +1236,42 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", @@ -1670,15 +1686,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.17" @@ -1785,7 +1792,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -1831,9 +1838,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1852,9 +1859,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.61" +version = "0.10.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -1873,7 +1880,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -1884,9 +1891,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", @@ -1937,9 +1944,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "predicates" @@ -2010,9 +2017,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] @@ -2037,7 +2044,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -2400,9 +2407,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6" +checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" [[package]] name = "semver" @@ -2456,7 +2463,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -2637,9 +2644,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -2715,7 +2722,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -2814,7 +2821,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", ] [[package]] @@ -2949,7 +2956,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", "wasm-bindgen-shared", ] @@ -3006,7 +3013,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.43", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3169,7 +3176,7 @@ dependencies = [ "lazy_static", "libc", "mach", - "memoffset 0.8.0", + "memoffset", "more-asserts", "region", "scopeguard", diff --git a/Cargo.toml b/Cargo.toml index 8796233..57de6f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ nibiru-macro = { path = "packages/nibiru-macro" } bash-rs = { path = "packages/bash-rs" } # deps: CosmWasm -cosmwasm-std = { version = "1.5.0", features = ["stargate"] } +cosmwasm-std = { version = "1.5.0", features = ["stargate", "staking"] } cosmwasm-schema = "1.5.0" cw-storage-plus = { version = "1.2.0" } cw-multi-test = { version = "0.20.0" } diff --git a/contracts/airdrop/.cargo/config b/contracts/airdrop/.cargo/config new file mode 100644 index 0000000..b613a59 --- /dev/null +++ b/contracts/airdrop/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +schema = "run --example schema" diff --git a/contracts/airdrop/Cargo.toml b/contracts/airdrop/Cargo.toml new file mode 100644 index 0000000..c4ed42b --- /dev/null +++ b/contracts/airdrop/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "airdrop" +version = "0.0.1" +edition = "2021" +homepage = "https://nibiru.fi" +repository = "https://github.com/NibiruChain/cw-nibiru" +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[profile.release] +opt-level = 3 +debug = false +rpath = false +lto = true +debug-assertions = false +codegen-units = 1 +panic = 'abort' +incremental = false +overflow-checks = true + +[package.metadata.scripts] +optimize = """docker run --rm -v "$(pwd)":/code \ + -e CARGO_TERM_COLOR=always \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/optimizer:0.15.0 +""" + +[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 } +nibiru-std = { workspace = true } +thiserror = { workspace = true } +schemars = "0.8.15" +serde = { version = "1.0.188", default-features = false, features = ["derive"] } +cw-ownable = { workspace = true } +cw2 = { workspace = true } +semver = "1" + +[dev-dependencies] +anyhow = { workspace = true } \ No newline at end of file diff --git a/contracts/airdrop/README.md b/contracts/airdrop/README.md new file mode 100644 index 0000000..fae09cc --- /dev/null +++ b/contracts/airdrop/README.md @@ -0,0 +1,13 @@ +# Airdrop Contract + +## Overview + +The airdrop contract is used to distribute tokens to a list of addresses. An instance of a contract represents a campaign. The contract is initialized with a campaign id, a campaign name, a campaign description, an owner (the deployer of the contract), a list of managers, and funds which become the unallocated amount. + +The token allocation amount starts unallocated and eventually gets allocated to users by the owner and managers. + +The contract owner and managers can allocate/reward users with tokens by calling the `reward_users` function. The `reward_users` function takes a list of addresses and amounts. The total reward amount must be less than the unallocated token amount of the contract. + +## Withdraw + +Only the contract owner can withdraw from the contract (not the managers). The `withdraw` exists to withdraw any leftover tokens after the campaign has ended. There is no check for if the total outstanding reward amount is greater than the amount of funds left in the contract. `withdraw` should only be called after the campaign ends because it could leave the contract in a state where it cannot fulfill a user's outstanding reward amount. Additional funds can be sent to the contract to reverse the withdrawal. diff --git a/contracts/airdrop/src/contract.rs b/contracts/airdrop/src/contract.rs new file mode 100644 index 0000000..471345b --- /dev/null +++ b/contracts/airdrop/src/contract.rs @@ -0,0 +1,256 @@ +use crate::{ + msg::{ + ExecuteMsg, InstantiateMsg, QueryMsg, RewardUserRequest, + RewardUserResponse, + }, + state::{Campaign, CAMPAIGN, USER_REWARDS}, +}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, + Empty, Env, MessageInfo, Response, StdError, StdResult, Uint128, +}; +use cw2::{get_contract_version, set_contract_version}; +use semver::Version; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + if info.funds.len() != 1 { + return Err(StdError::generic_err("Only one coin is allowed")); + } + + let bond_denom = deps.querier.query_bonded_denom()?; + let coin = info.funds.get(0).unwrap(); + if coin.denom != bond_denom { + return Err(StdError::generic_err("Only native tokens are allowed")); + } + + let campaign = Campaign { + campaign_id: msg.campaign_id, + campaign_name: msg.campaign_name, + campaign_description: msg.campaign_description, + owner: info.sender.clone(), + managers: msg.managers, + unallocated_amount: coin.amount, + }; + CAMPAIGN.save(deps.storage, &campaign)?; + + return Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("owner", info.sender)); +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + deps: DepsMut, + _env: Env, + _msg: Empty, +) -> Result { + let new_version: Version = CONTRACT_VERSION + .parse() + .map_err(|_| StdError::generic_err("Invalid contract version format"))?; + let current_version = get_contract_version(deps.storage)?; + + if current_version.contract != CONTRACT_NAME { + return Err(StdError::generic_err( + "Can only upgrade from same contract type", + )); + } + + if current_version.version.parse::().unwrap() >= new_version { + return Err(StdError::generic_err( + "Cannot upgrade from a newer contract version", + )); + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new().add_attribute("method", "migrate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::RewardUsers { requests } => { + reward_users(deps, env, info, requests) + } + ExecuteMsg::Claim {} => claim(deps, env, info), + ExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount), + } +} + +pub fn reward_users( + deps: DepsMut, + _env: Env, + info: MessageInfo, + requests: Vec, +) -> Result { + let mut res = vec![]; + + for req in requests { + let mut campaign = CAMPAIGN.load(deps.storage).map_err(|_| { + StdError::generic_err("Failed to load campaign data") + })?; + + if campaign.owner != info.sender + && !campaign.managers.contains(&info.sender) + { + res.push(RewardUserResponse { + user_address: req.user_address.clone(), + success: false, + error_msg: "Unauthorized".to_string(), + }); + continue; + } + + if campaign.unallocated_amount < req.amount { + res.push(RewardUserResponse { + user_address: req.user_address.clone(), + success: false, + error_msg: "Not enough funds in campaign".to_string(), + }); + continue; + } + + match USER_REWARDS.may_load(deps.storage, req.user_address.clone())? { + Some(mut user_reward) => { + user_reward += req.amount; + USER_REWARDS.save( + deps.storage, + req.user_address.clone(), + &user_reward, + )?; + } + None => { + USER_REWARDS.save( + deps.storage, + req.user_address.clone(), + &req.amount, + )?; + } + }; + campaign.unallocated_amount -= req.amount; + CAMPAIGN.save(deps.storage, &campaign)?; + + res.push(RewardUserResponse { + user_address: req.user_address.clone(), + success: true, + error_msg: "".to_string(), + }); + } + + return Ok(Response::new() + .add_attribute("method", "reward_users") + .set_data(to_json_binary(&res).unwrap())); +} + +pub fn claim( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + let bond_denom = deps.querier.query_bonded_denom()?; + + match USER_REWARDS.may_load(deps.storage, info.sender.clone())? { + Some(user_reward) => { + USER_REWARDS + .remove(deps.storage, info.sender.clone()); + + Ok(Response::new() + .add_attribute("method", "claim") + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: bond_denom.clone(), + amount: user_reward, + }], + }))) + } + None => Err(StdError::generic_err("User pool does not exist")), + } +} + +pub fn withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, +) -> Result { + let campaign = CAMPAIGN.load(deps.storage)?; + + if info.sender != campaign.owner { + return Err(StdError::generic_err("Only contract owner can withdraw")); + } + + let bond_denom = deps.querier.query_bonded_denom()?; + + let own_balance: Uint128 = deps + .querier + .query_balance(&env.contract.address, bond_denom.clone()) + .map_err(|_| StdError::generic_err("Failed to query contract balance"))? + .amount; + + if amount > own_balance { + return Err(StdError::generic_err("Not enough funds in the contract")); + } + + let res = Response::new() + .add_attribute("method", "withdraw") + .add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: bond_denom.clone(), + amount, + }], + })); + + return Ok(res); +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Campaign {} => query_campaign(deps, env), + QueryMsg::GetUserReward { user_address } => { + query_user_reward(deps, env, user_address) + } + } +} + +pub fn query_campaign(deps: Deps, _env: Env) -> StdResult { + match CAMPAIGN.load(deps.storage) { + Ok(campaign) => return to_json_binary(&campaign), + Err(_) => { + return Err(StdError::generic_err("Failed to load campaign data")) + } + } +} + +pub fn query_user_reward( + deps: Deps, + _env: Env, + user_address: Addr, +) -> StdResult { + match USER_REWARDS.load(deps.storage, user_address) { + Ok(user_reward) => return to_json_binary(&user_reward), + Err(_) => { + return Err(StdError::generic_err("User reward does not exist")) + } + }; +} diff --git a/contracts/airdrop/src/lib.rs b/contracts/airdrop/src/lib.rs new file mode 100644 index 0000000..b88f588 --- /dev/null +++ b/contracts/airdrop/src/lib.rs @@ -0,0 +1,6 @@ +pub mod contract; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; diff --git a/contracts/airdrop/src/msg.rs b/contracts/airdrop/src/msg.rs new file mode 100644 index 0000000..5cc1369 --- /dev/null +++ b/contracts/airdrop/src/msg.rs @@ -0,0 +1,43 @@ +use cosmwasm_std::{Uint128, Addr}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct InstantiateMsg { + pub campaign_id: String, + pub campaign_name: String, + pub campaign_description: String, + pub managers: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct RewardUserRequest { + pub user_address: Addr, + pub amount: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct RewardUserResponse { + pub user_address: Addr, + pub success: bool, + pub error_msg: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + RewardUsers { + requests: Vec + }, + Claim {}, + Withdraw { + amount: Uint128, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + Campaign { }, + GetUserReward { user_address: Addr }, +} + diff --git a/contracts/airdrop/src/state.rs b/contracts/airdrop/src/state.rs new file mode 100644 index 0000000..4296a65 --- /dev/null +++ b/contracts/airdrop/src/state.rs @@ -0,0 +1,18 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Map, Item}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Campaign { + pub campaign_id: String, + pub campaign_name: String, + pub campaign_description: String, + + pub unallocated_amount: Uint128, + pub owner: Addr, + pub managers: Vec, +} + +pub const CAMPAIGN: Item = Item::new("campaign"); +pub const USER_REWARDS: Map = Map::new("user_rewards"); diff --git a/contracts/airdrop/src/tests/execute/claim.rs b/contracts/airdrop/src/tests/execute/claim.rs new file mode 100644 index 0000000..a9b65fe --- /dev/null +++ b/contracts/airdrop/src/tests/execute/claim.rs @@ -0,0 +1,79 @@ +use crate::contract::{claim, instantiate, reward_users}; +use crate::msg::{InstantiateMsg, RewardUserRequest}; +use crate::state::{USER_REWARDS}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{coins, Addr, BankMsg, CosmosMsg, StdError, SubMsg, Uint128}; +use std::vec; + +#[test] +fn test_claim() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + reward_users( + deps.as_mut(), + env.clone(), + mock_info("owner", &[]), + vec![ + RewardUserRequest { + user_address: Addr::unchecked("user1"), + amount: Uint128::new(750), + }, + RewardUserRequest { + user_address: Addr::unchecked("user2"), + amount: Uint128::new(250), + }, + ], + ) + .unwrap(); + + // try to claim from user1 + let resp = + claim(deps.as_mut(), env.clone(), mock_info("user1", &[])).unwrap(); + + assert_eq!( + resp.messages, + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "user1".to_string(), + amount: coins(750, ""), + }))] + ); + assert_eq!( + USER_REWARDS.has(deps.as_ref().storage, Addr::unchecked("user1")), + false + ); + + // try to claim from user2 + let resp = + claim(deps.as_mut(), env.clone(), mock_info("user2", &[])).unwrap(); + + assert_eq!( + resp.messages, + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "user2".to_string(), + amount: coins(250, ""), + }))] + ); + assert_eq!( + USER_REWARDS.has(deps.as_ref().storage, Addr::unchecked("user2")), + false + ); + + // try to claim from user3 who doesn't exist + let resp = claim(deps.as_mut(), env.clone(), mock_info("user3", &[])); + + assert_eq!(resp, Err(StdError::generic_err("User pool does not exist"))); +} diff --git a/contracts/airdrop/src/tests/execute/mod.rs b/contracts/airdrop/src/tests/execute/mod.rs new file mode 100644 index 0000000..38d240a --- /dev/null +++ b/contracts/airdrop/src/tests/execute/mod.rs @@ -0,0 +1,3 @@ +mod claim; +mod reward_users; +mod withdraw; \ No newline at end of file diff --git a/contracts/airdrop/src/tests/execute/reward_users.rs b/contracts/airdrop/src/tests/execute/reward_users.rs new file mode 100644 index 0000000..6d3f1b8 --- /dev/null +++ b/contracts/airdrop/src/tests/execute/reward_users.rs @@ -0,0 +1,173 @@ +use crate::contract::{instantiate, reward_users}; +use crate::msg::{InstantiateMsg, RewardUserRequest, RewardUserResponse}; +use crate::state::{Campaign, CAMPAIGN, USER_REWARDS}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{coins, from_json, Addr, Uint128}; +use std::vec; + +#[test] +fn test_reward_users_fully_allocated() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + let resp = reward_users( + deps.as_mut(), + env.clone(), + mock_info("owner", &[]), + vec![ + RewardUserRequest { + user_address: Addr::unchecked("user1"), + amount: Uint128::new(750), + }, + RewardUserRequest { + user_address: Addr::unchecked("user2"), + amount: Uint128::new(250), + }, + ], + ) + .unwrap(); + + // assert response + let user_responses: Vec = + from_json(resp.data.unwrap()).unwrap(); + assert_eq!( + user_responses, + vec![ + RewardUserResponse { + user_address: Addr::unchecked("user1"), + success: true, + error_msg: "".to_string(), + }, + RewardUserResponse { + user_address: Addr::unchecked("user2"), + success: true, + error_msg: "".to_string(), + }, + ] + ); + + // assert inner state of the contract + let campaign = CAMPAIGN.load(deps.as_ref().storage).unwrap(); + assert_eq!( + campaign, + Campaign { + owner: Addr::unchecked("owner"), + unallocated_amount: Uint128::zero(), + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + } + ); + + assert_eq!( + USER_REWARDS + .load(deps.as_ref().storage, Addr::unchecked("user1")) + .unwrap(), + Uint128::new(750) + ); + + assert_eq!( + USER_REWARDS + .load(deps.as_ref().storage, Addr::unchecked("user2")) + .unwrap(), + Uint128::new(250) + ); +} + + +#[test] +fn test_reward_users_as_manager() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + let resp = reward_users( + deps.as_mut(), + env.clone(), + mock_info("manager1", &[]), + vec![ + RewardUserRequest { + user_address: Addr::unchecked("user1"), + amount: Uint128::new(750), + }, + RewardUserRequest { + user_address: Addr::unchecked("user2"), + amount: Uint128::new(250), + }, + ], + ) + .unwrap(); + + // assert response + let user_responses: Vec = + from_json(resp.data.unwrap()).unwrap(); + assert_eq!( + user_responses, + vec![ + RewardUserResponse { + user_address: Addr::unchecked("user1"), + success: true, + error_msg: "".to_string(), + }, + RewardUserResponse { + user_address: Addr::unchecked("user2"), + success: true, + error_msg: "".to_string(), + }, + ] + ); + + // assert inner state of the contract + let campaign = CAMPAIGN.load(deps.as_ref().storage).unwrap(); + assert_eq!( + campaign, + Campaign { + owner: Addr::unchecked("owner"), + unallocated_amount: Uint128::zero(), + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + } + ); + + assert_eq!( + USER_REWARDS + .load(deps.as_ref().storage, Addr::unchecked("user1")) + .unwrap(), + Uint128::new(750) + ); + + assert_eq!( + USER_REWARDS + .load(deps.as_ref().storage, Addr::unchecked("user2")) + .unwrap(), + Uint128::new(250) + ); +} diff --git a/contracts/airdrop/src/tests/execute/withdraw.rs b/contracts/airdrop/src/tests/execute/withdraw.rs new file mode 100644 index 0000000..5144f57 --- /dev/null +++ b/contracts/airdrop/src/tests/execute/withdraw.rs @@ -0,0 +1,106 @@ +use crate::contract::{instantiate, withdraw}; +use crate::msg::InstantiateMsg; +use cosmwasm_std::testing::{ + mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, +}; +use cosmwasm_std::{coins, BankMsg, CosmosMsg, StdError, SubMsg, Uint128, Addr}; +use std::vec; + +#[test] +fn test_withdraw_ok() { + let mut deps = mock_dependencies_with_balance(&coins(1000, "")); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + // try to withdraw + let resp = withdraw( + deps.as_mut(), + env.clone(), + mock_info("owner", &[]), + Uint128::new(1000), + ) + .unwrap(); + + assert_eq!( + resp.messages, + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "owner".to_string(), + amount: coins(1000, ""), + }))] + ); +} + +#[test] +fn test_withdraw_too_much() { + let mut deps = mock_dependencies_with_balance(&coins(1000, "")); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + // try to withdraw + let resp = withdraw( + deps.as_mut(), + env.clone(), + mock_info("owner", &[]), + Uint128::new(1001), + ); + + assert_eq!( + resp, + Err(StdError::generic_err("Not enough funds in the contract")) + ); +} + +#[test] +fn test_withdraw_unauthorized() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + // try to withdraw + let res = withdraw( + deps.as_mut(), + env.clone(), + mock_info("not_owner", &[]), + Uint128::new(1000), + ); + assert_eq!( + res, + Err(StdError::generic_err("Only contract owner can withdraw")) + ); +} diff --git a/contracts/airdrop/src/tests/instantiate.rs b/contracts/airdrop/src/tests/instantiate.rs new file mode 100644 index 0000000..393e553 --- /dev/null +++ b/contracts/airdrop/src/tests/instantiate.rs @@ -0,0 +1,66 @@ +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{Uint128, Addr, coins, StdError}; + +use crate::contract::instantiate; +use crate::msg::InstantiateMsg; +use crate::state::{Campaign, CAMPAIGN}; + +#[test] +fn test_instantiate() { + let mut deps = mock_dependencies(); + let info = mock_info("sender", &coins(1000, "")); + let env = mock_env(); + let msg = InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }; + + instantiate(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); + + let campaign = CAMPAIGN.load(deps.as_ref().storage).unwrap(); + assert_eq!( + campaign, + Campaign { + owner: Addr::unchecked("sender"), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + unallocated_amount: Uint128::new(1000), + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + } + ); +} + +#[test] +fn test_instantiate_with_no_funds() { + let mut deps = mock_dependencies(); + let info = mock_info("sender", &[]); + let env = mock_env(); + let msg = InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }; + + let resp = instantiate(deps.as_mut(), env.clone(), info.clone(), msg.clone()); + assert_eq!(resp, Err(StdError::generic_err("Only one coin is allowed"))); +} + +#[test] +fn test_instantiate_with_invalid_denom() { + let mut deps = mock_dependencies(); + let info = mock_info("sender", &coins(1000, "foo")); + let env = mock_env(); + let msg = InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }; + + let resp = instantiate(deps.as_mut(), env.clone(), info.clone(), msg.clone()); + assert_eq!(resp, Err(StdError::generic_err("Only native tokens are allowed"))); +} \ No newline at end of file diff --git a/contracts/airdrop/src/tests/mod.rs b/contracts/airdrop/src/tests/mod.rs new file mode 100644 index 0000000..0d532e5 --- /dev/null +++ b/contracts/airdrop/src/tests/mod.rs @@ -0,0 +1,3 @@ +mod instantiate; +mod execute; +mod query; \ No newline at end of file diff --git a/contracts/airdrop/src/tests/query/campaign.rs b/contracts/airdrop/src/tests/query/campaign.rs new file mode 100644 index 0000000..bacd199 --- /dev/null +++ b/contracts/airdrop/src/tests/query/campaign.rs @@ -0,0 +1,48 @@ +use cosmwasm_std::testing::{ + mock_dependencies, mock_env, mock_info, +}; +use cosmwasm_std::{ + coins, from_json, Addr, Uint128, +}; + +use crate::contract::{ + instantiate, query_campaign, +}; +use crate::msg:: + InstantiateMsg +; +use crate::state::{Campaign}; + +#[test] +fn test_query_campaign() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + let res = + query_campaign(deps.as_ref(), env.clone()).unwrap(); + let campaign: Campaign = from_json(res).unwrap(); + assert_eq!( + campaign, + Campaign { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + owner: Addr::unchecked("owner"), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + unallocated_amount: Uint128::new(1000), + } + ); +} \ No newline at end of file diff --git a/contracts/airdrop/src/tests/query/mod.rs b/contracts/airdrop/src/tests/query/mod.rs new file mode 100644 index 0000000..f0340ec --- /dev/null +++ b/contracts/airdrop/src/tests/query/mod.rs @@ -0,0 +1,2 @@ +mod campaign; +mod user_pool; \ No newline at end of file diff --git a/contracts/airdrop/src/tests/query/user_pool.rs b/contracts/airdrop/src/tests/query/user_pool.rs new file mode 100644 index 0000000..e650845 --- /dev/null +++ b/contracts/airdrop/src/tests/query/user_pool.rs @@ -0,0 +1,64 @@ +use crate::contract::{instantiate, query_user_reward, reward_users}; +use crate::msg::{InstantiateMsg, RewardUserRequest}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{coins, from_json, Addr, StdError, Uint128}; +use std::vec; + +#[test] +fn test_query_user_pool() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + reward_users( + deps.as_mut(), + env.clone(), + mock_info("owner", &[]), + vec![RewardUserRequest { + user_address: Addr::unchecked("user1"), + amount: Uint128::new(999), + }], + ) + .unwrap(); + + let res = + query_user_reward(deps.as_ref(), env.clone(), Addr::unchecked("user1")) + .unwrap(); + let user_pool: Uint128 = from_json(res).unwrap(); + assert_eq!(user_pool, Uint128::new(999)); +} + +#[test] +fn test_query_user_pool_empty() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + let res = + query_user_reward(deps.as_ref(), env.clone(), Addr::unchecked("user1")); + assert_eq!(res, Err(StdError::generic_err("User reward does not exist"))); +} From 1d422201bb76b2c71580d6a4a27b0ad02add1d1d Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Fri, 5 Jan 2024 17:31:57 +0100 Subject: [PATCH 3/5] add some refactors, part 1 --- contracts/airdrop/src/contract.rs | 34 ++++++++----------- .../airdrop/src/tests/execute/reward_users.rs | 5 +++ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/contracts/airdrop/src/contract.rs b/contracts/airdrop/src/contract.rs index 471345b..a4e04bb 100644 --- a/contracts/airdrop/src/contract.rs +++ b/contracts/airdrop/src/contract.rs @@ -12,6 +12,7 @@ use cosmwasm_std::{ Empty, Env, MessageInfo, Response, StdError, StdResult, Uint128, }; use cw2::{get_contract_version, set_contract_version}; +use cw2::VersionError::Std; use semver::Version; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -103,29 +104,22 @@ pub fn reward_users( ) -> Result { let mut res = vec![]; + let mut campaign = CAMPAIGN.load(deps.storage).map_err(|_| { + StdError::generic_err("Failed to load campaign data") + })?; + + if campaign.owner != info.sender + && !campaign.managers.contains(&info.sender) + { + return Err(StdError::generic_err("Unauthorized")); + } + for req in requests { - let mut campaign = CAMPAIGN.load(deps.storage).map_err(|_| { - StdError::generic_err("Failed to load campaign data") - })?; - - if campaign.owner != info.sender - && !campaign.managers.contains(&info.sender) - { - res.push(RewardUserResponse { - user_address: req.user_address.clone(), - success: false, - error_msg: "Unauthorized".to_string(), - }); - continue; - } if campaign.unallocated_amount < req.amount { - res.push(RewardUserResponse { - user_address: req.user_address.clone(), - success: false, - error_msg: "Not enough funds in campaign".to_string(), - }); - continue; + return Err(StdError::generic_err( + "Not enough funds in the campaign", + )); } match USER_REWARDS.may_load(deps.storage, req.user_address.clone())? { diff --git a/contracts/airdrop/src/tests/execute/reward_users.rs b/contracts/airdrop/src/tests/execute/reward_users.rs index 6d3f1b8..57ac5bd 100644 --- a/contracts/airdrop/src/tests/execute/reward_users.rs +++ b/contracts/airdrop/src/tests/execute/reward_users.rs @@ -171,3 +171,8 @@ fn test_reward_users_as_manager() { Uint128::new(250) ); } + +fn test_fails_when_we_try_to_allocate_more_than_available() { + let mut deps = mock_dependencies(); + let env = mock_env(); +} \ No newline at end of file From 0f337d1b16f125056beaf76b0b2cc1f2f3f48b00 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Fri, 5 Jan 2024 17:39:21 +0100 Subject: [PATCH 4/5] it fails when there are not enough funds --- contracts/airdrop/src/contract.rs | 1 - .../airdrop/src/tests/execute/reward_users.rs | 40 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/contracts/airdrop/src/contract.rs b/contracts/airdrop/src/contract.rs index a4e04bb..aeac361 100644 --- a/contracts/airdrop/src/contract.rs +++ b/contracts/airdrop/src/contract.rs @@ -12,7 +12,6 @@ use cosmwasm_std::{ Empty, Env, MessageInfo, Response, StdError, StdResult, Uint128, }; use cw2::{get_contract_version, set_contract_version}; -use cw2::VersionError::Std; use semver::Version; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); diff --git a/contracts/airdrop/src/tests/execute/reward_users.rs b/contracts/airdrop/src/tests/execute/reward_users.rs index 57ac5bd..8da802c 100644 --- a/contracts/airdrop/src/tests/execute/reward_users.rs +++ b/contracts/airdrop/src/tests/execute/reward_users.rs @@ -2,7 +2,7 @@ use crate::contract::{instantiate, reward_users}; use crate::msg::{InstantiateMsg, RewardUserRequest, RewardUserResponse}; use crate::state::{Campaign, CAMPAIGN, USER_REWARDS}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{coins, from_json, Addr, Uint128}; +use cosmwasm_std::{coins, from_json, Addr, Uint128, StdError}; use std::vec; #[test] @@ -172,7 +172,45 @@ fn test_reward_users_as_manager() { ); } +#[test] fn test_fails_when_we_try_to_allocate_more_than_available() { let mut deps = mock_dependencies(); let env = mock_env(); + + instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &coins(1000, "")), + InstantiateMsg { + campaign_id: "campaign_id".to_string(), + campaign_name: "campaign_name".to_string(), + campaign_description: "campaign_description".to_string(), + managers: vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + }, + ) + .unwrap(); + + let resp = reward_users( + deps.as_mut(), + env.clone(), + mock_info("manager1", &[]), + vec![ + RewardUserRequest { + user_address: Addr::unchecked("user1"), + amount: Uint128::new(750), + }, + RewardUserRequest { + user_address: Addr::unchecked("user2"), + amount: Uint128::new(250), + }, + RewardUserRequest { + user_address: Addr::unchecked("user3"), + amount: Uint128::new(251), + }, + ], + ); + + assert_eq!(resp, Err(StdError::generic_err( + "Not enough funds in the campaign", + ))); } \ No newline at end of file From 902b78892bb847c5410067238d3037b32de6e5a9 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Fri, 5 Jan 2024 17:41:16 +0100 Subject: [PATCH 5/5] remove unused imports --- nibiru-std/src/proto/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/nibiru-std/src/proto/mod.rs b/nibiru-std/src/proto/mod.rs index 4311de8..cfdf394 100644 --- a/nibiru-std/src/proto/mod.rs +++ b/nibiru-std/src/proto/mod.rs @@ -5,8 +5,6 @@ mod type_url_cosmos; mod type_url_nibiru; pub use traits::*; -pub use type_url_cosmos::*; -pub use type_url_nibiru::*; pub mod cosmos { /// Authentication of accounts and transactions.