From d0966eef51da69a1aedd5fe09fe7729bd7639f79 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 24 Apr 2024 12:20:38 +0300 Subject: [PATCH 01/28] feat: conditional splitter building blocks --- Cargo.lock | 16 + .../.cargo/config | 4 + .../andromeda-conditional-splitter/.gitignore | 2 + .../andromeda-conditional-splitter/Cargo.toml | 32 + .../examples/schema.rs | 11 + .../schema/andromeda-splitter.json | 1348 +++++++++++++++++ .../schema/raw/execute.json | 650 ++++++++ .../schema/raw/instantiate.json | 108 ++ .../schema/raw/query.json | 185 +++ .../raw/response_to_a_d_o_base_version.json | 14 + .../schema/raw/response_to_andr_hook.json | 6 + .../schema/raw/response_to_andr_query.json | 181 +++ .../schema/raw/response_to_app_contract.json | 20 + .../schema/raw/response_to_balance.json | 39 + ...esponse_to_block_height_upon_creation.json | 16 + .../raw/response_to_get_splitter_config.json | 110 ++ .../schema/raw/response_to_is_operator.json | 14 + .../raw/response_to_kernel_address.json | 20 + .../schema/raw/response_to_module.json | 32 + .../schema/raw/response_to_module_ids.json | 8 + .../schema/raw/response_to_operators.json | 17 + .../raw/response_to_original_publisher.json | 14 + .../schema/raw/response_to_owner.json | 14 + .../raw/response_to_ownership_request.json | 40 + .../raw/response_to_permissioned_actions.json | 8 + .../schema/raw/response_to_permissions.json | 112 ++ .../schema/raw/response_to_type.json | 14 + .../schema/raw/response_to_version.json | 14 + .../src/contract.rs | 343 +++++ .../andromeda-conditional-splitter/src/lib.rs | 8 + .../src/mock.rs | 59 + .../src/state.rs | 6 + .../src/testing/mock_querier.rs | 173 +++ .../src/testing/mod.rs | 2 + .../src/testing/tests.rs | 506 +++++++ .../src/conditional_splitter.rs | 247 +++ packages/andromeda-finance/src/lib.rs | 1 + packages/std/src/error.rs | 6 + tests-integration/Cargo.toml | 3 + 39 files changed, 4403 insertions(+) create mode 100644 contracts/finance/andromeda-conditional-splitter/.cargo/config create mode 100644 contracts/finance/andromeda-conditional-splitter/.gitignore create mode 100644 contracts/finance/andromeda-conditional-splitter/Cargo.toml create mode 100644 contracts/finance/andromeda-conditional-splitter/examples/schema.rs create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/andromeda-splitter.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/instantiate.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/query.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_a_d_o_base_version.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_hook.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_query.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_app_contract.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_balance.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_block_height_upon_creation.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_splitter_config.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_is_operator.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_kernel_address.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module_ids.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_operators.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_original_publisher.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_owner.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_ownership_request.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissioned_actions.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_type.json create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_version.json create mode 100644 contracts/finance/andromeda-conditional-splitter/src/contract.rs create mode 100644 contracts/finance/andromeda-conditional-splitter/src/lib.rs create mode 100644 contracts/finance/andromeda-conditional-splitter/src/mock.rs create mode 100644 contracts/finance/andromeda-conditional-splitter/src/state.rs create mode 100644 contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs create mode 100644 contracts/finance/andromeda-conditional-splitter/src/testing/mod.rs create mode 100644 contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs create mode 100644 packages/andromeda-finance/src/conditional_splitter.rs diff --git a/Cargo.lock b/Cargo.lock index ea1712654..36dfe6fea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,21 @@ dependencies = [ "cw721 0.18.0", ] +[[package]] +name = "andromeda-conditional-splitter" +version = "1.0.0" +dependencies = [ + "andromeda-app", + "andromeda-finance", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", +] + [[package]] name = "andromeda-cross-chain-swap" version = "1.0.0" @@ -1761,6 +1776,7 @@ dependencies = [ "andromeda-app", "andromeda-app-contract", "andromeda-auction", + "andromeda-conditional-splitter", "andromeda-crowdfund", "andromeda-cw20", "andromeda-cw20-staking", diff --git a/contracts/finance/andromeda-conditional-splitter/.cargo/config b/contracts/finance/andromeda-conditional-splitter/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/finance/andromeda-conditional-splitter/.gitignore b/contracts/finance/andromeda-conditional-splitter/.gitignore new file mode 100644 index 000000000..869df07da --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock \ No newline at end of file diff --git a/contracts/finance/andromeda-conditional-splitter/Cargo.toml b/contracts/finance/andromeda-conditional-splitter/Cargo.toml new file mode 100644 index 000000000..6ee00edb9 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "andromeda-conditional-splitter" +version = "1.0.0" +edition = "2021" +rust-version = "1.75.0" + +[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 = [] +testing = ["cw-multi-test", "andromeda-testing"] + + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } + +andromeda-std = { workspace = true } +andromeda-finance = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } + +[dev-dependencies] +andromeda-app = { workspace = true } diff --git a/contracts/finance/andromeda-conditional-splitter/examples/schema.rs b/contracts/finance/andromeda-conditional-splitter/examples/schema.rs new file mode 100644 index 000000000..fb68c826a --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/examples/schema.rs @@ -0,0 +1,11 @@ +use andromeda_finance::splitter::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/andromeda-splitter.json b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-splitter.json new file mode 100644 index 000000000..13f4006c7 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-splitter.json @@ -0,0 +1,1348 @@ +{ + "contract_name": "andromeda-splitter", + "contract_version": "1.0.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "kernel_address", + "recipients" + ], + "properties": { + "kernel_address": { + "type": "string" + }, + "lock_time": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "recipients": { + "description": "The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on their assigned percentage.", + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + } + }, + "additionalProperties": false, + "definitions": { + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + }, + "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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Recipient": { + "description": "A simple struct used for inter-contract communication. The struct can be used in two ways:\n\n1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address\n\nThe `Binary` message can be any message that the contract at the resolved address can handle.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "ibc_recovery_address": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Update the recipients list. Only executable by the contract owner when the contract is not locked.", + "type": "object", + "required": [ + "update_recipients" + ], + "properties": { + "update_recipients": { + "type": "object", + "required": [ + "recipients" + ], + "properties": { + "recipients": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Used to lock/unlock the contract allowing the config to be updated.", + "type": "object", + "required": [ + "update_lock" + ], + "properties": { + "update_lock": { + "type": "object", + "required": [ + "lock_time" + ], + "properties": { + "lock_time": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Divides any attached funds to the message amongst the recipients list.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "amp_receive" + ], + "properties": { + "amp_receive": { + "$ref": "#/definitions/AMPPkt" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "$ref": "#/definitions/OwnershipMessage" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_kernel_address" + ], + "properties": { + "update_kernel_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_app_contract" + ], + "properties": { + "update_app_contract": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permissioning" + ], + "properties": { + "permissioning": { + "$ref": "#/definitions/PermissioningMessage" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "AMPCtx": { + "type": "object", + "required": [ + "id", + "origin", + "previous_sender" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "origin": { + "type": "string" + }, + "origin_username": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "previous_sender": { + "type": "string" + } + }, + "additionalProperties": false + }, + "AMPMsg": { + "description": "This struct defines how the kernel parses and relays messages between ADOs If the desired recipient is via IBC then namespacing must be employed The attached message must be a binary encoded execute message for the receiving ADO Funds can be attached for an individual message and will be attached accordingly", + "type": "object", + "required": [ + "config", + "funds", + "message", + "recipient" + ], + "properties": { + "config": { + "description": "When the message should reply, defaults to Always", + "allOf": [ + { + "$ref": "#/definitions/AMPMsgConfig" + } + ] + }, + "funds": { + "description": "Any funds to be attached to the message, defaults to an empty vector", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "message": { + "description": "The message to be sent to the recipient", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "recipient": { + "description": "The message recipient, can be a contract/wallet address or a namespaced URI", + "allOf": [ + { + "$ref": "#/definitions/AndrAddr" + } + ] + } + }, + "additionalProperties": false + }, + "AMPMsgConfig": { + "description": "The configuration of the message to be sent.\n\nUsed when a sub message is generated for the given AMP Msg (only used in the case of Wasm Messages).", + "type": "object", + "required": [ + "direct", + "exit_at_error", + "reply_on" + ], + "properties": { + "direct": { + "description": "Whether to send the message directly to the given recipient", + "type": "boolean" + }, + "exit_at_error": { + "description": "Determines whether the operation should terminate or proceed upon a failed message", + "type": "boolean" + }, + "gas_limit": { + "description": "An optional imposed gas limit for the message", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "ibc_config": { + "anyOf": [ + { + "$ref": "#/definitions/IBCConfig" + }, + { + "type": "null" + } + ] + }, + "reply_on": { + "description": "When the message should reply, defaults to Always", + "allOf": [ + { + "$ref": "#/definitions/ReplyOn" + } + ] + } + }, + "additionalProperties": false + }, + "AMPPkt": { + "description": "An Andromeda packet contains all message protocol related data, this is what is sent between ADOs when communicating It contains an original sender, if used for authorisation the sender must be authorised The previous sender is the one who sent the message A packet may contain several messages which allows for message batching", + "type": "object", + "required": [ + "ctx", + "messages" + ], + "properties": { + "ctx": { + "$ref": "#/definitions/AMPCtx" + }, + "messages": { + "description": "Any messages associated with the packet", + "type": "array", + "items": { + "$ref": "#/definitions/AMPMsg" + } + } + }, + "additionalProperties": false + }, + "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" + }, + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + }, + "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" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "IBCConfig": { + "type": "object", + "properties": { + "recovery_addr": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "OwnershipMessage": { + "oneOf": [ + { + "type": "string", + "enum": [ + "revoke_ownership_offer", + "accept_ownership", + "disown" + ] + }, + { + "type": "object", + "required": [ + "update_owner" + ], + "properties": { + "update_owner": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Permission": { + "description": "An enum to represent a user's permission for an action\n\n- **Blacklisted** - The user cannot perform the action until after the provided expiration - **Limited** - The user can perform the action while uses are remaining and before the provided expiration **for a permissioned action** - **Whitelisted** - The user can perform the action until the provided expiration **for a permissioned action**\n\nExpiration defaults to `Never` if not provided", + "oneOf": [ + { + "type": "object", + "required": [ + "blacklisted" + ], + "properties": { + "blacklisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "limited" + ], + "properties": { + "limited": { + "type": "object", + "required": [ + "uses" + ], + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "uses": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "PermissioningMessage": { + "oneOf": [ + { + "type": "object", + "required": [ + "set_permission" + ], + "properties": { + "set_permission": { + "type": "object", + "required": [ + "action", + "actor", + "permission" + ], + "properties": { + "action": { + "type": "string" + }, + "actor": { + "$ref": "#/definitions/AndrAddr" + }, + "permission": { + "$ref": "#/definitions/Permission" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_permission" + ], + "properties": { + "remove_permission": { + "type": "object", + "required": [ + "action", + "actor" + ], + "properties": { + "action": { + "type": "string" + }, + "actor": { + "$ref": "#/definitions/AndrAddr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permission_action" + ], + "properties": { + "permission_action": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "disable_action_permissioning" + ], + "properties": { + "disable_action_permissioning": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Recipient": { + "description": "A simple struct used for inter-contract communication. The struct can be used in two ways:\n\n1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address\n\nThe `Binary` message can be any message that the contract at the resolved address can handle.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "ibc_recovery_address": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ReplyOn": { + "description": "Use this to define when the contract gets a response callback. If you only need it for errors or success you can select just those in order to save gas.", + "oneOf": [ + { + "description": "Always perform a callback after SubMsg is processed", + "type": "string", + "enum": [ + "always" + ] + }, + { + "description": "Only callback if SubMsg returned an error, no callback on success case", + "type": "string", + "enum": [ + "error" + ] + }, + { + "description": "Only callback if SubMsg was successful, no callback on error case", + "type": "string", + "enum": [ + "success" + ] + }, + { + "description": "Never make a callback - this is like the original CosmosMsg semantics", + "type": "string", + "enum": [ + "never" + ] + } + ] + }, + "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": "The current config of the Splitter contract", + "type": "object", + "required": [ + "get_splitter_config" + ], + "properties": { + "get_splitter_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership_request" + ], + "properties": { + "ownership_request": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kernel_address" + ], + "properties": { + "kernel_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "app_contract" + ], + "properties": { + "app_contract": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "original_publisher" + ], + "properties": { + "original_publisher": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "block_height_upon_creation" + ], + "properties": { + "block_height_upon_creation": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "a_d_o_base_version" + ], + "properties": { + "a_d_o_base_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "permissions": { + "type": "object", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string" + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permissioned_actions" + ], + "properties": { + "permissioned_actions": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "a_d_o_base_version": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ADOBaseVersionResponse", + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + }, + "additionalProperties": false + }, + "app_contract": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AppContractResponse", + "type": "object", + "required": [ + "app_contract" + ], + "properties": { + "app_contract": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "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" + } + } + }, + "block_height_upon_creation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BlockHeightResponse", + "type": "object", + "required": [ + "block_height" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "get_splitter_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetSplitterConfigResponse", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/Splitter" + } + }, + "additionalProperties": false, + "definitions": { + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + }, + "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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Recipient": { + "description": "A simple struct used for inter-contract communication. The struct can be used in two ways:\n\n1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address\n\nThe `Binary` message can be any message that the contract at the resolved address can handle.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "ibc_recovery_address": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Splitter": { + "description": "A config struct for a `Splitter` contract.", + "type": "object", + "required": [ + "lock", + "recipients" + ], + "properties": { + "lock": { + "description": "The lock's expiration time", + "allOf": [ + { + "$ref": "#/definitions/Milliseconds" + } + ] + }, + "recipients": { + "description": "The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on their assigned percentage.", + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + } + }, + "additionalProperties": false + } + } + }, + "kernel_address": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "KernelAddressResponse", + "type": "object", + "required": [ + "kernel_address" + ], + "properties": { + "kernel_address": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "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" + } + } + }, + "original_publisher": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PublisherResponse", + "type": "object", + "required": [ + "original_publisher" + ], + "properties": { + "original_publisher": { + "type": "string" + } + }, + "additionalProperties": false + }, + "owner": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractOwnerResponse", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ownership_request": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractPotentialOwnerResponse", + "type": "object", + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "potential_owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "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" + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "permissioned_actions": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_PermissionInfo", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionInfo" + }, + "definitions": { + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Permission": { + "description": "An enum to represent a user's permission for an action\n\n- **Blacklisted** - The user cannot perform the action until after the provided expiration - **Limited** - The user can perform the action while uses are remaining and before the provided expiration **for a permissioned action** - **Whitelisted** - The user can perform the action until the provided expiration **for a permissioned action**\n\nExpiration defaults to `Never` if not provided", + "oneOf": [ + { + "type": "object", + "required": [ + "blacklisted" + ], + "properties": { + "blacklisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "limited" + ], + "properties": { + "limited": { + "type": "object", + "required": [ + "uses" + ], + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "uses": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "PermissionInfo": { + "type": "object", + "required": [ + "action", + "actor", + "permission" + ], + "properties": { + "action": { + "type": "string" + }, + "actor": { + "type": "string" + }, + "permission": { + "$ref": "#/definitions/Permission" + } + }, + "additionalProperties": false + } + } + }, + "type": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TypeResponse", + "type": "object", + "required": [ + "ado_type" + ], + "properties": { + "ado_type": { + "type": "string" + } + }, + "additionalProperties": false + }, + "version": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VersionResponse", + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json new file mode 100644 index 000000000..ac72d4ab6 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json @@ -0,0 +1,650 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Update the recipients list. Only executable by the contract owner when the contract is not locked.", + "type": "object", + "required": [ + "update_recipients" + ], + "properties": { + "update_recipients": { + "type": "object", + "required": [ + "recipients" + ], + "properties": { + "recipients": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Used to lock/unlock the contract allowing the config to be updated.", + "type": "object", + "required": [ + "update_lock" + ], + "properties": { + "update_lock": { + "type": "object", + "required": [ + "lock_time" + ], + "properties": { + "lock_time": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Divides any attached funds to the message amongst the recipients list.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "amp_receive" + ], + "properties": { + "amp_receive": { + "$ref": "#/definitions/AMPPkt" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "$ref": "#/definitions/OwnershipMessage" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_kernel_address" + ], + "properties": { + "update_kernel_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_app_contract" + ], + "properties": { + "update_app_contract": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permissioning" + ], + "properties": { + "permissioning": { + "$ref": "#/definitions/PermissioningMessage" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "AMPCtx": { + "type": "object", + "required": [ + "id", + "origin", + "previous_sender" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "origin": { + "type": "string" + }, + "origin_username": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "previous_sender": { + "type": "string" + } + }, + "additionalProperties": false + }, + "AMPMsg": { + "description": "This struct defines how the kernel parses and relays messages between ADOs If the desired recipient is via IBC then namespacing must be employed The attached message must be a binary encoded execute message for the receiving ADO Funds can be attached for an individual message and will be attached accordingly", + "type": "object", + "required": [ + "config", + "funds", + "message", + "recipient" + ], + "properties": { + "config": { + "description": "When the message should reply, defaults to Always", + "allOf": [ + { + "$ref": "#/definitions/AMPMsgConfig" + } + ] + }, + "funds": { + "description": "Any funds to be attached to the message, defaults to an empty vector", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "message": { + "description": "The message to be sent to the recipient", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "recipient": { + "description": "The message recipient, can be a contract/wallet address or a namespaced URI", + "allOf": [ + { + "$ref": "#/definitions/AndrAddr" + } + ] + } + }, + "additionalProperties": false + }, + "AMPMsgConfig": { + "description": "The configuration of the message to be sent.\n\nUsed when a sub message is generated for the given AMP Msg (only used in the case of Wasm Messages).", + "type": "object", + "required": [ + "direct", + "exit_at_error", + "reply_on" + ], + "properties": { + "direct": { + "description": "Whether to send the message directly to the given recipient", + "type": "boolean" + }, + "exit_at_error": { + "description": "Determines whether the operation should terminate or proceed upon a failed message", + "type": "boolean" + }, + "gas_limit": { + "description": "An optional imposed gas limit for the message", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "ibc_config": { + "anyOf": [ + { + "$ref": "#/definitions/IBCConfig" + }, + { + "type": "null" + } + ] + }, + "reply_on": { + "description": "When the message should reply, defaults to Always", + "allOf": [ + { + "$ref": "#/definitions/ReplyOn" + } + ] + } + }, + "additionalProperties": false + }, + "AMPPkt": { + "description": "An Andromeda packet contains all message protocol related data, this is what is sent between ADOs when communicating It contains an original sender, if used for authorisation the sender must be authorised The previous sender is the one who sent the message A packet may contain several messages which allows for message batching", + "type": "object", + "required": [ + "ctx", + "messages" + ], + "properties": { + "ctx": { + "$ref": "#/definitions/AMPCtx" + }, + "messages": { + "description": "Any messages associated with the packet", + "type": "array", + "items": { + "$ref": "#/definitions/AMPMsg" + } + } + }, + "additionalProperties": false + }, + "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" + }, + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + }, + "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" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "IBCConfig": { + "type": "object", + "properties": { + "recovery_addr": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "OwnershipMessage": { + "oneOf": [ + { + "type": "string", + "enum": [ + "revoke_ownership_offer", + "accept_ownership", + "disown" + ] + }, + { + "type": "object", + "required": [ + "update_owner" + ], + "properties": { + "update_owner": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Permission": { + "description": "An enum to represent a user's permission for an action\n\n- **Blacklisted** - The user cannot perform the action until after the provided expiration - **Limited** - The user can perform the action while uses are remaining and before the provided expiration **for a permissioned action** - **Whitelisted** - The user can perform the action until the provided expiration **for a permissioned action**\n\nExpiration defaults to `Never` if not provided", + "oneOf": [ + { + "type": "object", + "required": [ + "blacklisted" + ], + "properties": { + "blacklisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "limited" + ], + "properties": { + "limited": { + "type": "object", + "required": [ + "uses" + ], + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "uses": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "PermissioningMessage": { + "oneOf": [ + { + "type": "object", + "required": [ + "set_permission" + ], + "properties": { + "set_permission": { + "type": "object", + "required": [ + "action", + "actor", + "permission" + ], + "properties": { + "action": { + "type": "string" + }, + "actor": { + "$ref": "#/definitions/AndrAddr" + }, + "permission": { + "$ref": "#/definitions/Permission" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_permission" + ], + "properties": { + "remove_permission": { + "type": "object", + "required": [ + "action", + "actor" + ], + "properties": { + "action": { + "type": "string" + }, + "actor": { + "$ref": "#/definitions/AndrAddr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permission_action" + ], + "properties": { + "permission_action": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "disable_action_permissioning" + ], + "properties": { + "disable_action_permissioning": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Recipient": { + "description": "A simple struct used for inter-contract communication. The struct can be used in two ways:\n\n1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address\n\nThe `Binary` message can be any message that the contract at the resolved address can handle.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "ibc_recovery_address": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ReplyOn": { + "description": "Use this to define when the contract gets a response callback. If you only need it for errors or success you can select just those in order to save gas.", + "oneOf": [ + { + "description": "Always perform a callback after SubMsg is processed", + "type": "string", + "enum": [ + "always" + ] + }, + { + "description": "Only callback if SubMsg returned an error, no callback on success case", + "type": "string", + "enum": [ + "error" + ] + }, + { + "description": "Only callback if SubMsg was successful, no callback on error case", + "type": "string", + "enum": [ + "success" + ] + }, + { + "description": "Never make a callback - this is like the original CosmosMsg semantics", + "type": "string", + "enum": [ + "never" + ] + } + ] + }, + "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/finance/andromeda-conditional-splitter/schema/raw/instantiate.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/instantiate.json new file mode 100644 index 000000000..6ead203e7 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/instantiate.json @@ -0,0 +1,108 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "kernel_address", + "recipients" + ], + "properties": { + "kernel_address": { + "type": "string" + }, + "lock_time": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "recipients": { + "description": "The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on their assigned percentage.", + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + } + }, + "additionalProperties": false, + "definitions": { + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + }, + "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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Recipient": { + "description": "A simple struct used for inter-contract communication. The struct can be used in two ways:\n\n1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address\n\nThe `Binary` message can be any message that the contract at the resolved address can handle.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "ibc_recovery_address": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/query.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/query.json new file mode 100644 index 000000000..c06bee85f --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/query.json @@ -0,0 +1,185 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "The current config of the Splitter contract", + "type": "object", + "required": [ + "get_splitter_config" + ], + "properties": { + "get_splitter_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership_request" + ], + "properties": { + "ownership_request": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kernel_address" + ], + "properties": { + "kernel_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "app_contract" + ], + "properties": { + "app_contract": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "original_publisher" + ], + "properties": { + "original_publisher": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "block_height_upon_creation" + ], + "properties": { + "block_height_upon_creation": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "a_d_o_base_version" + ], + "properties": { + "a_d_o_base_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "permissions": { + "type": "object", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string" + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permissioned_actions" + ], + "properties": { + "permissioned_actions": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_a_d_o_base_version.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_a_d_o_base_version.json new file mode 100644 index 000000000..d346a8fe4 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_a_d_o_base_version.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ADOBaseVersionResponse", + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_hook.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_hook.json new file mode 100644 index 000000000..a46573aa3 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_hook.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "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" +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_query.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_query.json new file mode 100644 index 000000000..606833093 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_andr_query.json @@ -0,0 +1,181 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AndromedaQuery", + "oneOf": [ + { + "type": "object", + "required": [ + "get" + ], + "properties": { + "get": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kernel_address" + ], + "properties": { + "kernel_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "original_publisher" + ], + "properties": { + "original_publisher": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "block_height_upon_creation" + ], + "properties": { + "block_height_upon_creation": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_operator" + ], + "properties": { + "is_operator": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "module" + ], + "properties": { + "module": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "$ref": "#/definitions/Uint64" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "module_ids" + ], + "properties": { + "module_ids": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_app_contract.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_app_contract.json new file mode 100644 index 000000000..2f221101f --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_app_contract.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AppContractResponse", + "type": "object", + "required": [ + "app_contract" + ], + "properties": { + "app_contract": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "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" + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_balance.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_balance.json new file mode 100644 index 000000000..4e3b16aa4 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_balance.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BalanceResponse", + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "Always returns a Coin with the requested denom. This may be of 0 amount if no such funds.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "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" + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_block_height_upon_creation.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_block_height_upon_creation.json new file mode 100644 index 000000000..0ef1730a5 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_block_height_upon_creation.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BlockHeightResponse", + "type": "object", + "required": [ + "block_height" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_splitter_config.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_splitter_config.json new file mode 100644 index 000000000..0298ebde4 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_splitter_config.json @@ -0,0 +1,110 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetSplitterConfigResponse", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/Splitter" + } + }, + "additionalProperties": false, + "definitions": { + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + }, + "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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Recipient": { + "description": "A simple struct used for inter-contract communication. The struct can be used in two ways:\n\n1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address\n\nThe `Binary` message can be any message that the contract at the resolved address can handle.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "ibc_recovery_address": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Splitter": { + "description": "A config struct for a `Splitter` contract.", + "type": "object", + "required": [ + "lock", + "recipients" + ], + "properties": { + "lock": { + "description": "The lock's expiration time", + "allOf": [ + { + "$ref": "#/definitions/Milliseconds" + } + ] + }, + "recipients": { + "description": "The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on their assigned percentage.", + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_is_operator.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_is_operator.json new file mode 100644 index 000000000..9f750c867 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_is_operator.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IsOperatorResponse", + "type": "object", + "required": [ + "is_operator" + ], + "properties": { + "is_operator": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_kernel_address.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_kernel_address.json new file mode 100644 index 000000000..dc0e342cd --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_kernel_address.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "KernelAddressResponse", + "type": "object", + "required": [ + "kernel_address" + ], + "properties": { + "kernel_address": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "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" + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module.json new file mode 100644 index 000000000..c7b506914 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Module", + "description": "A struct describing a token module, provided with the instantiation message this struct is used to record the info about the module and how/if it should be instantiated", + "type": "object", + "required": [ + "address", + "is_mutable" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "is_mutable": { + "type": "boolean" + }, + "name": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module_ids.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module_ids.json new file mode 100644 index 000000000..4290cb1a2 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_module_ids.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_operators.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_operators.json new file mode 100644 index 000000000..c812b4b84 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_operators.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorsResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_original_publisher.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_original_publisher.json new file mode 100644 index 000000000..2269741f1 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_original_publisher.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PublisherResponse", + "type": "object", + "required": [ + "original_publisher" + ], + "properties": { + "original_publisher": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_owner.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_owner.json new file mode 100644 index 000000000..cd196f7c4 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_owner.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractOwnerResponse", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_ownership_request.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_ownership_request.json new file mode 100644 index 000000000..ddcf6f03b --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_ownership_request.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractPotentialOwnerResponse", + "type": "object", + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "potential_owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "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" + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissioned_actions.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissioned_actions.json new file mode 100644 index 000000000..4290cb1a2 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissioned_actions.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json new file mode 100644 index 000000000..545578ae5 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_PermissionInfo", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionInfo" + }, + "definitions": { + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Permission": { + "description": "An enum to represent a user's permission for an action\n\n- **Blacklisted** - The user cannot perform the action until after the provided expiration - **Limited** - The user can perform the action while uses are remaining and before the provided expiration **for a permissioned action** - **Whitelisted** - The user can perform the action until the provided expiration **for a permissioned action**\n\nExpiration defaults to `Never` if not provided", + "oneOf": [ + { + "type": "object", + "required": [ + "blacklisted" + ], + "properties": { + "blacklisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "limited" + ], + "properties": { + "limited": { + "type": "object", + "required": [ + "uses" + ], + "properties": { + "expiration": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + }, + "uses": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "whitelisted": { + "anyOf": [ + { + "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "PermissionInfo": { + "type": "object", + "required": [ + "action", + "actor", + "permission" + ], + "properties": { + "action": { + "type": "string" + }, + "actor": { + "type": "string" + }, + "permission": { + "$ref": "#/definitions/Permission" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_type.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_type.json new file mode 100644 index 000000000..27e96a9e8 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_type.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TypeResponse", + "type": "object", + "required": [ + "ado_type" + ], + "properties": { + "ado_type": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_version.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_version.json new file mode 100644 index 000000000..4a16f1056 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_version.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VersionResponse", + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs new file mode 100644 index 000000000..4437c193b --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -0,0 +1,343 @@ +use std::{ops::Add, vec}; + +use crate::state::CONDITIONAL_SPLITTER; +use andromeda_finance::conditional_splitter::{ + find_threshold, validate_recipient_list, AddressFunds, ConditionalSplitter, ExecuteMsg, + GetConditionalSplitterConfigResponse, InstantiateMsg, QueryMsg, +}; + +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + amp::messages::AMPPkt, + common::{actions::call_action, encode_binary, Milliseconds, MillisecondsDuration}, + error::ContractError, +}; +use andromeda_std::{ado_contract::ADOContract, common::context::ExecuteContext}; +use cosmwasm_std::{ + attr, ensure, entry_point, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Reply, Response, StdError, SubMsg, Uint128, +}; +use cw_utils::nonpayable; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:andromeda-conditional-splitter"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +// 1 day in seconds +const ONE_DAY: u64 = 86_400; +// 1 year in seconds +const ONE_YEAR: u64 = 31_536_000; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let current_time = Milliseconds::from_seconds(env.block.time.seconds()); + + // Construct Address Fund, funds automatically set to 0 + let mut address_funds: Vec = vec![]; + for recipient in msg.recipients { + address_funds.push(AddressFunds::new(recipient)) + } + + let mut conditional_splitter = ConditionalSplitter { + recipients: address_funds, + thresholds: msg.thresholds.clone(), + lock: msg.lock_time.unwrap_or_default(), + }; + // Validate recipient list and thresholds + conditional_splitter.validate(deps.as_ref())?; + + match msg.lock_time { + Some(lock_time) => { + // New lock time can't be too short + ensure!( + lock_time.seconds() >= ONE_DAY, + ContractError::LockTimeTooShort {} + ); + // New lock time can't be too long + ensure!( + lock_time.seconds() <= ONE_YEAR, + ContractError::LockTimeTooLong {} + ); + conditional_splitter.lock = current_time.plus_milliseconds(lock_time); + } + None => { + conditional_splitter.lock = Milliseconds::default(); + } + } + // Save kernel address after validating it + CONDITIONAL_SPLITTER.save(deps.storage, &conditional_splitter)?; + + let inst_resp = ADOContract::default().instantiate( + deps.storage, + env, + deps.api, + &deps.querier, + info, + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address.clone(), + owner: msg.owner.clone(), + }, + )?; + + Ok(inst_resp) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + if msg.result.is_err() { + return Err(ContractError::Std(StdError::generic_err( + msg.result.unwrap_err(), + ))); + } + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let ctx = ExecuteContext::new(deps, info, env); + + match msg { + ExecuteMsg::AMPReceive(pkt) => { + ADOContract::default().execute_amp_receive(ctx, pkt, handle_execute) + } + _ => handle_execute(ctx, msg), + } +} + +pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + let action_response = call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + let res = match msg { + // ExecuteMsg::UpdateRecipients { recipients } => execute_update_recipients(ctx, recipients), + ExecuteMsg::UpdateLock { lock_time } => execute_update_lock(ctx, lock_time), + ExecuteMsg::Send {} => execute_send(ctx), + _ => ADOContract::default().execute(ctx, msg), + }?; + Ok(res + .add_submessages(action_response.messages) + .add_attributes(action_response.attributes) + .add_events(action_response.events)) +} + +fn execute_send(ctx: ExecuteContext) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + + ensure!( + !info.funds.is_empty(), + ContractError::InvalidFunds { + msg: "At least one coin should to be sent".to_string(), + } + ); + ensure!( + info.funds.len() == 1, + ContractError::ExceedsMaxAllowedCoins {} + ); + for coin in info.funds.clone() { + ensure!( + !coin.amount.is_zero(), + ContractError::InvalidFunds { + msg: "Amount must be non-zero".to_string(), + } + ); + } + + let conditional_splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; + + let mut msgs: Vec = Vec::new(); + let mut amp_funds: Vec = Vec::new(); + + let mut remainder_funds = info.funds.clone(); + + let mut pkt = AMPPkt::from_ctx(ctx.amp_ctx, ctx.env.contract.address.to_string()); + let mut recipients_with_new_funds: Vec = vec![]; + + for recipient_addr in &conditional_splitter.recipients { + // Get current range + let threshold = find_threshold(&conditional_splitter.thresholds, recipient_addr.funds)?; + let recipient_percent = threshold.percentage; + + let mut vec_coin: Vec = Vec::new(); + for (i, coin) in info.funds.clone().iter().enumerate() { + let mut recip_coin: Coin = coin.clone(); + + // Difference between the range's max and current funds received + let till_threshold = threshold.range.max - recipient_addr.funds; + + // If info.funds is greater than the below number, it means that the threshold will be surpassed. + //TODO Multiply till_threshold with the current threshold's percentage, the additional funds (info.funds - till_threshold) will use the next threshold's percentage. + let funds_surpass_threshold = + till_threshold.checked_div_floor(recipient_percent).unwrap(); + + // Save new amount sent + recip_coin.amount = coin.amount * recipient_percent; + + // Save new funds + let new_fund = recipient_addr.funds + recip_coin.amount; + let new_address_funds = AddressFunds { + recipient: recipient_addr.recipient.clone(), + funds: new_fund, + }; + recipients_with_new_funds.push(new_address_funds); + + remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; + vec_coin.push(recip_coin.clone()); + amp_funds.push(recip_coin); + } + + let amp_msg = recipient_addr + .recipient + .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; + pkt = pkt.add_message(amp_msg); + } + let new_conditional_splitter = ConditionalSplitter { + recipients: recipients_with_new_funds, + thresholds: conditional_splitter.thresholds, + lock: conditional_splitter.lock, + }; + CONDITIONAL_SPLITTER.save(deps.storage, &new_conditional_splitter)?; + + remainder_funds.retain(|x| x.amount > Uint128::zero()); + + // Why does the remaining funds go the the sender of the executor of the splitter? + // Is it considered tax(fee) or mistake? + // Discussion around caller of splitter function in andromedaSPLITTER smart contract. + // From tests, it looks like owner of smart contract (Andromeda) will recieve the rest of funds. + // If so, should be documented + if !remainder_funds.is_empty() { + msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: remainder_funds, + }))); + } + let kernel_address = ADOContract::default().get_kernel_address(deps.as_ref().storage)?; + let distro_msg = pkt.to_sub_msg(kernel_address, Some(amp_funds), 1)?; + msgs.push(distro_msg); + + Ok(Response::new() + .add_submessages(msgs) + .add_attribute("action", "send") + .add_attribute("sender", info.sender.to_string())) +} + +// fn execute_update_recipients( +// ctx: ExecuteContext, +// recipients: Vec, +// ) -> Result { +// let ExecuteContext { +// deps, info, env, .. +// } = ctx; + +// nonpayable(&info)?; + +// ensure!( +// ADOContract::default().is_owner_or_operator(deps.storage, info.sender.as_str())?, +// ContractError::Unauthorized {} +// ); + +// validate_recipient_list(deps.as_ref(), recipients.clone())?; + +// let mut splitter = SPLITTER.load(deps.storage)?; +// // Can't call this function while the lock isn't expired + +// ensure!( +// splitter.lock.is_expired(&env.block), +// ContractError::ContractLocked {} +// ); +// // Max 100 recipients +// ensure!( +// recipients.len() <= 100, +// ContractError::ReachedRecipientLimit {} +// ); + +// splitter.recipients = recipients; +// SPLITTER.save(deps.storage, &splitter)?; + +// Ok(Response::default().add_attributes(vec![attr("action", "update_recipients")])) +// } + +fn execute_update_lock( + ctx: ExecuteContext, + lock_time: MillisecondsDuration, +) -> Result { + let ExecuteContext { + deps, info, env, .. + } = ctx; + + nonpayable(&info)?; + + ensure!( + ADOContract::default().is_owner_or_operator(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + let mut splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; + + // Can't call this function while the lock isn't expired + + ensure!( + splitter.lock.is_expired(&env.block), + ContractError::ContractLocked {} + ); + // Get current time + let current_time = Milliseconds::from_seconds(env.block.time.seconds()); + + // New lock time can't be too short + ensure!( + lock_time.seconds() >= ONE_DAY, + ContractError::LockTimeTooShort {} + ); + + // New lock time can't be unreasonably long + ensure!( + lock_time.seconds() <= ONE_YEAR, + ContractError::LockTimeTooLong {} + ); + + // Set new lock time + let new_expiration = current_time.plus_milliseconds(lock_time); + + splitter.lock = new_expiration; + + CONDITIONAL_SPLITTER.save(deps.storage, &splitter)?; + + Ok(Response::default().add_attributes(vec![ + attr("action", "update_lock"), + attr("locked", new_expiration.to_string()), + ])) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetSplitterConfig {} => encode_binary(&query_splitter(deps)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +fn query_splitter(deps: Deps) -> Result { + let splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; + + Ok(GetConditionalSplitterConfigResponse { config: splitter }) +} diff --git a/contracts/finance/andromeda-conditional-splitter/src/lib.rs b/contracts/finance/andromeda-conditional-splitter/src/lib.rs new file mode 100644 index 000000000..abf4af66c --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +pub mod state; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; + +#[cfg(test)] +mod testing; diff --git a/contracts/finance/andromeda-conditional-splitter/src/mock.rs b/contracts/finance/andromeda-conditional-splitter/src/mock.rs new file mode 100644 index 000000000..1ed3bd44a --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/src/mock.rs @@ -0,0 +1,59 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] + +use crate::contract::{execute, instantiate, query, reply}; +use andromeda_finance::splitter::{AddressPercent, ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::common::Milliseconds; +use andromeda_testing::{ + mock::MockApp, mock_ado, mock_contract::ExecuteResult, MockADO, MockContract, +}; +use cosmwasm_std::{Addr, Coin, Empty}; +use cw_multi_test::{Contract, ContractWrapper, Executor}; + +pub struct MockSplitter(Addr); +mock_ado!(MockSplitter, ExecuteMsg, QueryMsg); + +impl MockSplitter { + pub fn instantiate( + app: &mut MockApp, + code_id: u64, + sender: Addr, + recipients: Vec, + kernel_address: impl Into, + lock_time: Option, + owner: Option, + ) -> Self { + let msg = mock_splitter_instantiate_msg(recipients, kernel_address, lock_time, owner); + let res = app.instantiate_contract(code_id, sender, &msg, &[], "Andromeda Splitter", None); + + Self(res.unwrap()) + } + + pub fn execute_send(&self, app: &mut MockApp, sender: Addr, funds: &[Coin]) -> ExecuteResult { + let msg = mock_splitter_send_msg(); + + self.execute(app, &msg, sender, funds) + } +} + +pub fn mock_andromeda_splitter() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply); + Box::new(contract) +} + +pub fn mock_splitter_instantiate_msg( + recipients: Vec, + kernel_address: impl Into, + lock_time: Option, + owner: Option, +) -> InstantiateMsg { + InstantiateMsg { + recipients, + lock_time: lock_time.map(Milliseconds), + kernel_address: kernel_address.into(), + owner, + } +} + +pub fn mock_splitter_send_msg() -> ExecuteMsg { + ExecuteMsg::Send {} +} diff --git a/contracts/finance/andromeda-conditional-splitter/src/state.rs b/contracts/finance/andromeda-conditional-splitter/src/state.rs new file mode 100644 index 000000000..1e03113c8 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/src/state.rs @@ -0,0 +1,6 @@ +use andromeda_finance::conditional_splitter::ConditionalSplitter; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; + +pub const CONDITIONAL_SPLITTER: Item = Item::new("conditional_splitter"); +pub const KERNEL_ADDRESS: Item = Item::new("kernel_address"); diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs new file mode 100644 index 000000000..6bad40e34 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs @@ -0,0 +1,173 @@ +use andromeda_std::ado_base::hooks::{AndromedaHook, HookMsg, OnFundsTransferResponse}; +use andromeda_std::ado_base::InstantiateMsg; +use andromeda_std::ado_contract::ADOContract; +use andromeda_std::common::Funds; +use andromeda_std::testing::mock_querier::MockAndromedaQuerier; +use cosmwasm_std::testing::mock_info; +use cosmwasm_std::{ + from_json, + testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, + to_json_binary, Binary, Coin, ContractResult, OwnedDeps, Querier, QuerierResult, QueryRequest, + SystemError, SystemResult, WasmQuery, +}; +use cosmwasm_std::{BankMsg, CosmosMsg, QuerierWrapper, Response, SubMsg, Uint128}; + +pub use andromeda_std::testing::mock_querier::{ + MOCK_ADDRESS_LIST_CONTRACT, MOCK_KERNEL_CONTRACT, MOCK_RATES_CONTRACT, +}; + +pub const MOCK_TAX_RECIPIENT: &str = "tax_recipient"; +pub const MOCK_ROYALTY_RECIPIENT: &str = "royalty_recipient"; + +/// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. +/// +/// Automatically assigns a kernel address as MOCK_KERNEL_CONTRACT. +pub fn mock_dependencies_custom( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + let storage = MockStorage::default(); + let mut deps = OwnedDeps { + storage, + api: MockApi::default(), + querier: custom_querier, + custom_query_type: std::marker::PhantomData, + }; + ADOContract::default() + .instantiate( + &mut deps.storage, + mock_env(), + &deps.api, + &QuerierWrapper::new(&deps.querier), + mock_info("sender", &[]), + InstantiateMsg { + ado_type: "splitter".to_string(), + ado_version: "test".to_string(), + + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + }, + ) + .unwrap(); + deps +} + +pub struct WasmMockQuerier { + pub base: MockQuerier, + pub contract_address: String, + pub tokens_left_to_burn: usize, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {e}"), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + match contract_addr.as_str() { + MOCK_RATES_CONTRACT => self.handle_rates_query(msg), + MOCK_ADDRESS_LIST_CONTRACT => self.handle_addresslist_query(msg), + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + + fn handle_rates_query(&self, msg: &Binary) -> QuerierResult { + match from_json(msg).unwrap() { + HookMsg::AndrHook(hook_msg) => match hook_msg { + AndromedaHook::OnFundsTransfer { + sender: _, + payload: _, + amount, + } => { + let (new_funds, msgs): (Funds, Vec) = match amount { + Funds::Native(ref coin) => ( + Funds::Native(Coin { + // Deduct royalty of 10%. + amount: coin.amount.multiply_ratio(90u128, 100u128), + denom: coin.denom.clone(), + }), + vec![ + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: MOCK_ROYALTY_RECIPIENT.to_owned(), + amount: vec![Coin { + // Royalty of 10% + amount: coin.amount.multiply_ratio(10u128, 100u128), + denom: coin.denom.clone(), + }], + })), + SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: MOCK_TAX_RECIPIENT.to_owned(), + amount: vec![Coin { + // Flat tax of 50 + amount: Uint128::from(50u128), + denom: coin.denom.clone(), + }], + })), + ], + ), + Funds::Cw20(_) => { + let resp: Response = Response::default(); + return SystemResult::Ok(ContractResult::Ok( + to_json_binary(&resp).unwrap(), + )); + } + }; + let response = OnFundsTransferResponse { + msgs, + events: vec![], + leftover_funds: new_funds, + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&Some(response)).unwrap())) + } + _ => SystemResult::Ok(ContractResult::Ok( + to_json_binary(&None::).unwrap(), + )), + }, + } + } + + fn handle_addresslist_query(&self, msg: &Binary) -> QuerierResult { + match from_json(msg).unwrap() { + HookMsg::AndrHook(hook_msg) => match hook_msg { + AndromedaHook::OnExecute { sender, payload: _ } => { + let whitelisted_addresses = ["sender"]; + let response: Response = Response::default(); + if whitelisted_addresses.contains(&sender.as_str()) { + SystemResult::Ok(ContractResult::Ok(to_json_binary(&response).unwrap())) + } else { + SystemResult::Ok(ContractResult::Err("InvalidAddress".to_string())) + } + } + _ => SystemResult::Ok(ContractResult::Ok( + to_json_binary(&None::).unwrap(), + )), + }, + } + } + + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + contract_address: mock_env().contract.address.to_string(), + tokens_left_to_burn: 2, + } + } +} diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/mod.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/mod.rs new file mode 100644 index 000000000..a1e507b68 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock_querier; +mod tests; diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs new file mode 100644 index 000000000..c8528a795 --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -0,0 +1,506 @@ +use andromeda_std::{ + amp::{ + messages::{AMPMsg, AMPPkt}, + recipient::Recipient, + }, + common::Milliseconds, + error::ContractError, +}; +use andromeda_testing::economics_msg::generate_economics_message; +use cosmwasm_std::{ + attr, from_json, + testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}, + to_json_binary, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Response, SubMsg, Uint128, +}; +pub const OWNER: &str = "creator"; + +use super::mock_querier::MOCK_KERNEL_CONTRACT; + +use crate::{ + contract::{execute, instantiate, query}, + state::CONDITIONAL_SPLITTER, + testing::mock_querier::mock_dependencies_custom, +}; +use andromeda_finance::conditional_splitter::{ + AddressFunds, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, + InstantiateMsg, QueryMsg, Range, Threshold, +}; + +fn init(deps: DepsMut) -> Response { + let mock_recipient: Vec = vec![AddressFunds { + recipient: Recipient::from_string(String::from("some_address")), + funds: Uint128::zero(), + }]; + let msg = InstantiateMsg { + owner: Some(OWNER.to_owned()), + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + recipients: vec![Recipient::from_string(String::from("some_address"))], + thresholds: vec![ + Threshold::new( + Range::new(Uint128::zero(), Uint128::new(10)), + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + Threshold::new( + Range::new(Uint128::new(11), Uint128::new(20)), + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + ], + lock_time: Some(Milliseconds::from_seconds(100_000)), + }; + + let info = mock_info("owner", &[]); + instantiate(deps, mock_env(), info, msg).unwrap() +} + +#[test] +fn test_instantiate() { + let mut deps = mock_dependencies_custom(&[]); + let res = init(deps.as_mut()); + assert_eq!(0, res.messages.len()); +} + +#[test] +fn test_execute_update_lock() { + let mut deps = mock_dependencies_custom(&[]); + let _res = init(deps.as_mut()); + + let env = mock_env(); + + let current_time = env.block.time.seconds(); + let lock_time = 100_000; + + // Start off with an expiration that's behind current time (expired) + let splitter = ConditionalSplitter { + recipients: vec![], + lock: Milliseconds::from_seconds(current_time - 1), + thresholds: vec![Threshold { + range: Range::new(Uint128::zero(), Uint128::new(10)), + percentage: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + }], + }; + + CONDITIONAL_SPLITTER + .save(deps.as_mut().storage, &splitter) + .unwrap(); + + let msg = ExecuteMsg::UpdateLock { + lock_time: Milliseconds::from_seconds(lock_time), + }; + + let info = mock_info(OWNER, &[]); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let new_lock = Milliseconds::from_seconds(current_time + lock_time); + assert_eq!( + Response::default() + .add_attributes(vec![ + attr("action", "update_lock"), + attr("locked", new_lock.to_string()) + ]) + .add_submessage(generate_economics_message(OWNER, "UpdateLock")), + res + ); + + //check result + let splitter = CONDITIONAL_SPLITTER.load(deps.as_ref().storage).unwrap(); + assert!(!splitter.lock.is_expired(&env.block)); + assert_eq!(new_lock, splitter.lock); +} + +// #[test] +// fn test_execute_update_recipients() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let _res = init(deps.as_mut()); + +// let splitter = ConditionalSplitter { +// recipients: vec![], +// lock: Milliseconds::from_seconds(0), +// }; + +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); + +// // Duplicate recipients +// let duplicate_recipients = vec![ +// AddressFunds { +// recipient: Recipient::from_string(String::from("addr1")), +// percent: Decimal::percent(40), +// }, +// AddressFunds { +// recipient: Recipient::from_string(String::from("addr1")), +// percent: Decimal::percent(60), +// }, +// ]; +// let msg = ExecuteMsg::UpdateRecipients { +// recipients: duplicate_recipients, +// }; + +// let info = mock_info(OWNER, &[]); +// let res = execute(deps.as_mut(), env.clone(), info, msg); +// assert_eq!(ContractError::DuplicateRecipient {}, res.unwrap_err()); + +// let recipients = vec![ +// AddressFunds { +// recipient: Recipient::from_string(String::from("addr1")), +// percent: Decimal::percent(40), +// }, +// AddressFunds { +// recipient: Recipient::from_string(String::from("addr2")), +// percent: Decimal::percent(60), +// }, +// ]; +// let msg = ExecuteMsg::UpdateRecipients { +// recipients: recipients.clone(), +// }; + +// let info = mock_info("incorrect_owner", &[]); +// let res = execute(deps.as_mut(), env.clone(), info, msg.clone()); +// assert_eq!(ContractError::Unauthorized {}, res.unwrap_err()); + +// let info = mock_info(OWNER, &[]); +// let res = execute(deps.as_mut(), env, info, msg).unwrap(); +// assert_eq!( +// Response::default() +// .add_attributes(vec![attr("action", "update_recipients")]) +// .add_submessage(generate_economics_message(OWNER, "UpdateRecipients")), +// res +// ); + +// //check result +// let splitter = CONDITIONAL_SPLITTER.load(deps.as_ref().storage).unwrap(); +// assert_eq!(splitter.recipients, recipients); +// } + +// #[test] +// fn test_execute_send() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let _res: Response = init(deps.as_mut()); + +// let sender_funds_amount = 10000u128; + +// let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); + +// let recip_address1 = "address1".to_string(); +// let recip_percent1 = 10; // 10% + +// let recip_address2 = "address2".to_string(); +// let recip_percent2 = 20; // 20% + +// let recip1 = Recipient::from_string(recip_address1); +// let recip2 = Recipient::from_string(recip_address2); + +// let recipient = vec![ +// AddressFunds { +// recipient: recip1.clone(), +// percent: Decimal::percent(recip_percent1), +// }, +// AddressFunds { +// recipient: recip2.clone(), +// percent: Decimal::percent(recip_percent2), +// }, +// ]; +// let msg = ExecuteMsg::Send {}; + +// let amp_msg_1 = recip1 +// .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(1000, "uluna")])) +// .unwrap(); +// let amp_msg_2 = recip2 +// .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(2000, "uluna")])) +// .unwrap(); +// let amp_pkt = AMPPkt::new( +// MOCK_CONTRACT_ADDR.to_string(), +// MOCK_CONTRACT_ADDR.to_string(), +// vec![amp_msg_1, amp_msg_2], +// ); +// let amp_msg = amp_pkt +// .to_sub_msg( +// MOCK_KERNEL_CONTRACT, +// Some(vec![Coin::new(1000, "uluna"), Coin::new(2000, "uluna")]), +// 1, +// ) +// .unwrap(); + +// let splitter = ConditionalSplitter { +// recipients: recipient, +// lock: Milliseconds::default(), +// thresholds: vec![Threshold { +// range: Range::new(Uint128::zero(), Uint128::new(10)), +// percentage: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), +// }], +// }; + +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); + +// let res = execute(deps.as_mut(), env, info, msg).unwrap(); + +// let expected_res = Response::new() +// .add_submessages(vec![ +// SubMsg::new( +// // refunds remainder to sender +// CosmosMsg::Bank(BankMsg::Send { +// to_address: OWNER.to_string(), +// amount: vec![Coin::new(7000, "uluna")], // 10000 * 0.7 remainder +// }), +// ), +// amp_msg, +// ]) +// .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) +// .add_submessage(generate_economics_message(OWNER, "Send")); + +// assert_eq!(res, expected_res); +// } + +// #[test] +// fn test_execute_send_ado_recipient() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let _res: Response = init(deps.as_mut()); + +// let sender_funds_amount = 10000u128; +// let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); + +// let recip_address1 = "address1".to_string(); +// let recip_percent1 = 10; // 10% + +// let recip_address2 = "address2".to_string(); +// let recip_percent2 = 20; // 20% + +// let recip1 = Recipient::from_string(recip_address1); +// let recip2 = Recipient::from_string(recip_address2); + +// let recipient = vec![ +// AddressFunds { +// recipient: recip1.clone(), +// percent: Decimal::percent(recip_percent1), +// }, +// AddressFunds { +// recipient: recip2.clone(), +// percent: Decimal::percent(recip_percent2), +// }, +// ]; +// let msg = ExecuteMsg::Send {}; + +// let amp_msg_1 = recip1 +// .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(1000, "uluna")])) +// .unwrap(); +// let amp_msg_2 = recip2 +// .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(2000, "uluna")])) +// .unwrap(); +// let amp_pkt = AMPPkt::new( +// MOCK_CONTRACT_ADDR.to_string(), +// MOCK_CONTRACT_ADDR.to_string(), +// vec![amp_msg_1, amp_msg_2], +// ); +// let amp_msg = amp_pkt +// .to_sub_msg( +// MOCK_KERNEL_CONTRACT, +// Some(vec![Coin::new(1000, "uluna"), Coin::new(2000, "uluna")]), +// 1, +// ) +// .unwrap(); + +// let splitter = ConditionalSplitter { +// recipients: recipient, +// lock: Milliseconds::default(), +// }; + +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); + +// let res = execute(deps.as_mut(), env, info.clone(), msg).unwrap(); + +// let expected_res = Response::new() +// .add_submessages(vec![ +// SubMsg::new( +// // refunds remainder to sender +// CosmosMsg::Bank(BankMsg::Send { +// to_address: info.sender.to_string(), +// amount: vec![Coin::new(7000, "uluna")], // 10000 * 0.7 remainder +// }), +// ), +// amp_msg, +// ]) +// .add_attribute("action", "send") +// .add_attribute("sender", "creator") +// .add_submessage(generate_economics_message(OWNER, "Send")); + +// assert_eq!(res, expected_res); +// } + +// #[test] +// fn test_handle_packet_exit_with_error_true() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let _res: Response = init(deps.as_mut()); + +// let sender_funds_amount = 0u128; +// let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); + +// let recip_address1 = "address1".to_string(); +// let recip_percent1 = 10; // 10% + +// let recip_percent2 = 20; // 20% + +// let recipient = vec![ +// AddressFunds { +// recipient: Recipient::from_string(recip_address1.clone()), +// percent: Decimal::percent(recip_percent1), +// }, +// AddressFunds { +// recipient: Recipient::from_string(recip_address1.clone()), +// percent: Decimal::percent(recip_percent2), +// }, +// ]; +// let pkt = AMPPkt::new( +// info.clone().sender, +// "cosmos2contract", +// vec![AMPMsg::new( +// recip_address1, +// to_json_binary(&ExecuteMsg::Send {}).unwrap(), +// Some(vec![Coin::new(0, "uluna")]), +// )], +// ); +// let msg = ExecuteMsg::AMPReceive(pkt); + +// let splitter = ConditionalSplitter { +// recipients: recipient, +// lock: Milliseconds::default(), +// }; + +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); + +// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + +// assert_eq!( +// err, +// ContractError::InvalidFunds { +// msg: "Amount must be non-zero".to_string(), +// } +// ); +// } + +// #[test] +// fn test_query_splitter() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let splitter = ConditionalSplitter { +// recipients: vec![], +// lock: Milliseconds::default(), +// }; + +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); + +// let query_msg = QueryMsg::GetConditionalSplitterConfig {}; +// let res = query(deps.as_ref(), env, query_msg).unwrap(); +// let val: GetConditionalSplitterConfigResponse = from_json(res).unwrap(); + +// assert_eq!(val.config, splitter); +// } + +// #[test] +// fn test_execute_send_error() { +// //Executes send with more than 5 tokens [ACK-04] +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let _res: Response = init(deps.as_mut()); + +// let sender_funds_amount = 10000u128; +// let owner = "creator"; +// let info = mock_info( +// owner, +// &vec![ +// Coin::new(sender_funds_amount, "uluna"), +// Coin::new(sender_funds_amount, "uluna"), +// Coin::new(sender_funds_amount, "uluna"), +// Coin::new(sender_funds_amount, "uluna"), +// Coin::new(sender_funds_amount, "uluna"), +// Coin::new(sender_funds_amount, "uluna"), +// ], +// ); + +// let recip_address1 = "address1".to_string(); +// let recip_percent1 = 10; // 10% + +// let recip_address2 = "address2".to_string(); +// let recip_percent2 = 20; // 20% + +// let recipient = vec![ +// AddressFunds { +// recipient: Recipient::from_string(recip_address1), +// percent: Decimal::percent(recip_percent1), +// }, +// AddressFunds { +// recipient: Recipient::from_string(recip_address2), +// percent: Decimal::percent(recip_percent2), +// }, +// ]; +// let msg = ExecuteMsg::Send {}; + +// let splitter = ConditionalSplitter { +// recipients: recipient, +// lock: Milliseconds::default(), +// }; + +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); + +// let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + +// let expected_res = ContractError::ExceedsMaxAllowedCoins {}; + +// assert_eq!(res, expected_res); +// } + +// #[test] +// fn test_update_app_contract() { +// let mut deps = mock_dependencies_custom(&[]); +// let _res: Response = init(deps.as_mut()); + +// let info = mock_info(OWNER, &[]); + +// let msg = ExecuteMsg::UpdateAppContract { +// address: "app_contract".to_string(), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "update_app_contract") +// .add_attribute("address", "app_contract") +// .add_submessage(generate_economics_message(OWNER, "UpdateAppContract")), +// res +// ); +// } + +// #[test] +// fn test_update_app_contract_invalid_recipient() { +// let mut deps = mock_dependencies_custom(&[]); +// let _res: Response = init(deps.as_mut()); + +// let info = mock_info(OWNER, &[]); + +// let msg = ExecuteMsg::UpdateAppContract { +// address: "z".to_string(), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg); + +// // assert_eq!( +// // ContractError::InvalidComponent { +// // name: "z".to_string() +// // }, +// // res.unwrap_err() +// // ); +// assert!(res.is_err()) +// } diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs new file mode 100644 index 000000000..32748375a --- /dev/null +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -0,0 +1,247 @@ +use andromeda_std::{ + amp::recipient::Recipient, + andr_exec, andr_instantiate, andr_query, + common::{MillisecondsDuration, MillisecondsExpiration}, + error::ContractError, +}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{ensure, Decimal, Deps, Uint128}; +use std::collections::HashSet; + +#[cw_serde] +// The range of received funds which will correspond to a certain percentage. +pub struct Range { + // Lower bound of the range + pub min: Uint128, + // Upper bound of the range + pub max: Uint128, +} +impl Range { + pub fn new(min: Uint128, max: Uint128) -> Self { + Self { min, max } + } + pub fn verify_range(&self) -> Result<(), ContractError> { + if self.min < self.max { + Ok(()) + } else { + Err(ContractError::InvalidRange {}) + } + } + pub fn contains(&self, num: Uint128) -> bool { + num >= self.min && num <= self.max + } +} + +// The contract owner will input a vector of Threshold +#[cw_serde] +pub struct Threshold { + pub range: Range, + pub percentage: Decimal, +} +impl Threshold { + pub fn new(range: Range, percentage: Decimal) -> Self { + Self { range, percentage } + } + pub fn contains(&self, num: Uint128) -> bool { + self.range.contains(num) + } +} + +pub fn find_threshold(thresholds: &[Threshold], num: Uint128) -> Result<&Threshold, ContractError> { + let threshold = thresholds.iter().find(|&threshold| threshold.contains(num)); + if let Some(threshold) = threshold { + Ok(threshold) + } else { + Err(ContractError::InvalidRange {}) + } +} + +#[cw_serde] +pub struct AddressFunds { + pub recipient: Recipient, + pub funds: Uint128, +} +impl AddressFunds { + pub fn new(recipient: Recipient) -> Self { + Self { + recipient, + funds: Uint128::zero(), + } + } +} + +#[cw_serde] +/// A config struct for a `Conditional Splitter` contract. +pub struct ConditionalSplitter { + /// The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on the threshold. + pub recipients: Vec, + /// The vector of thresholds which assign a percentage for a certain range of received funds + pub thresholds: Vec, + /// The lock's expiration time + pub lock: MillisecondsExpiration, +} +impl ConditionalSplitter { + pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { + validate_recipient_list(deps, self.recipients.clone())?; + validate_thresholds(&self.thresholds, &self.recipients) + } +} + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + /// The vector of recipients for the contract. Anytime a `Send` execute message is + /// sent the amount sent will be divided amongst these recipients depending on their assigned percentage. + pub recipients: Vec, + pub thresholds: Vec, + pub lock_time: Option, +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + /// Update the recipients list. Only executable by the contract owner when the contract is not locked. + UpdateRecipients { recipients: Vec }, + /// Used to lock/unlock the contract allowing the config to be updated. + UpdateLock { + // Milliseconds from current time + lock_time: MillisecondsDuration, + }, + /// Divides any attached funds to the message amongst the recipients list. + Send {}, +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// The current config of the Splitter contract + #[returns(GetConditionalSplitterConfigResponse)] + GetSplitterConfig {}, +} + +#[cw_serde] +pub struct GetConditionalSplitterConfigResponse { + pub config: ConditionalSplitter, +} + +/// Ensures that a given list of recipients for a `splitter` contract is valid: +/// +/// * Must include at least one recipient +/// * The number of recipients must not exceed 100 +/// * The recipient addresses must be unique +pub fn validate_recipient_list( + deps: Deps, + recipients: Vec, +) -> Result<(), ContractError> { + ensure!( + !recipients.is_empty(), + ContractError::EmptyRecipientsList {} + ); + ensure!( + recipients.len() <= 100, + ContractError::ReachedRecipientLimit {} + ); + let mut recipient_address_set = HashSet::new(); + + for recipient in recipients { + recipient.recipient.validate(&deps)?; + let recipient_address = recipient.recipient.address.get_raw_address(&deps)?; + ensure!( + !recipient_address_set.contains(&recipient_address), + ContractError::DuplicateRecipient {} + ); + recipient_address_set.insert(recipient_address); + } + + Ok(()) +} + +pub fn validate_thresholds( + thresholds: &Vec, + recipients: &Vec, +) -> Result<(), ContractError> { + let number_of_recipients = recipients.len() as u128; + let mut prev_max: Option = None; + + for threshold in thresholds { + // Check that the range is valid (min > max) + threshold.range.verify_range()?; + + // The percentage multiplied by the number of recipients shouldn't exceed 100 + ensure!( + threshold.percentage * Uint128::new(number_of_recipients) <= Uint128::new(100), + ContractError::AmountExceededHundredPrecent {} + ); + + // The ranges shouldn't overlap + if let Some(max) = prev_max { + if threshold.range.min <= max { + return Err(ContractError::OverlappingRanges {}); + } + } + + // Update prev_max + prev_max = Some(threshold.range.max); + } + + Ok(()) +} + +// #[cfg(test)] +// mod tests { +// use cosmwasm_std::testing::mock_dependencies; + +// use super::*; + +// #[test] +// fn test_validate_recipient_list() { +// let deps = mock_dependencies(); +// let empty_recipients = vec![]; +// let res = validate_recipient_list(deps.as_ref(), empty_recipients).unwrap_err(); +// assert_eq!(res, ContractError::EmptyRecipientsList {}); + +// let inadequate_recipients = vec![AddressPercent { +// recipient: Recipient::from_string(String::from("abc")), +// percent: Decimal::percent(150), +// }]; +// let res = validate_recipient_list(deps.as_ref(), inadequate_recipients).unwrap_err(); +// assert_eq!(res, ContractError::AmountExceededHundredPrecent {}); + +// let duplicate_recipients = vec![ +// AddressPercent { +// recipient: Recipient::from_string(String::from("abc")), +// percent: Decimal::percent(50), +// }, +// AddressPercent { +// recipient: Recipient::from_string(String::from("abc")), +// percent: Decimal::percent(50), +// }, +// ]; + +// let err = validate_recipient_list(deps.as_ref(), duplicate_recipients).unwrap_err(); +// assert_eq!(err, ContractError::DuplicateRecipient {}); + +// let valid_recipients = vec![ +// AddressPercent { +// recipient: Recipient::from_string(String::from("abc")), +// percent: Decimal::percent(50), +// }, +// AddressPercent { +// recipient: Recipient::from_string(String::from("xyz")), +// percent: Decimal::percent(50), +// }, +// ]; + +// let res = validate_recipient_list(deps.as_ref(), valid_recipients); +// assert!(res.is_ok()); + +// let one_valid_recipient = vec![AddressPercent { +// recipient: Recipient::from_string(String::from("abc")), +// percent: Decimal::percent(50), +// }]; + +// let res = validate_recipient_list(deps.as_ref(), one_valid_recipient); +// assert!(res.is_ok()); +// } +// } diff --git a/packages/andromeda-finance/src/lib.rs b/packages/andromeda-finance/src/lib.rs index b3d4a1461..7323bb3dd 100644 --- a/packages/andromeda-finance/src/lib.rs +++ b/packages/andromeda-finance/src/lib.rs @@ -1,3 +1,4 @@ +pub mod conditional_splitter; pub mod cross_chain_swap; pub mod rate_limiting_withdrawals; pub mod splitter; diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 280ff2627..b13ad2f50 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -44,6 +44,12 @@ pub enum ContractError { #[error("InvalidOrigin")] InvalidOrigin {}, + #[error("InvalidRange")] + InvalidRange {}, + + #[error("OverlappingRanges")] + OverlappingRanges {}, + #[error("Invalid {operation} Operation with {validator}")] InvalidValidatorOperation { operation: String, diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml index f809783a9..18f5961b7 100644 --- a/tests-integration/Cargo.toml +++ b/tests-integration/Cargo.toml @@ -71,6 +71,9 @@ andromeda-finance = { workspace = true } andromeda-splitter = { path = "../contracts/finance/andromeda-splitter", features = [ "testing", ] } +andromeda-conditional-splitter = { path = "../contracts/finance/andromeda-conditional-splitter", features = [ + "testing", +] } andromeda-vesting = { path = "../contracts/finance/andromeda-vesting", features = [ "testing", ] } From 172852dfbc0e267ba0cbdf5ccfac2455085c125e Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 24 Apr 2024 14:29:42 +0300 Subject: [PATCH 02/28] refactor: remove Range struct, modify Threshold to use min instead of range --- .../src/contract.rs | 12 ++- .../src/state.rs | 4 +- .../src/testing/tests.rs | 8 +- .../src/conditional_splitter.rs | 76 +++++++------------ packages/std/src/error.rs | 3 + 5 files changed, 40 insertions(+), 63 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 4437c193b..7352b93ce 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -1,10 +1,9 @@ -use std::{ops::Add, vec}; - use crate::state::CONDITIONAL_SPLITTER; use andromeda_finance::conditional_splitter::{ - find_threshold, validate_recipient_list, AddressFunds, ConditionalSplitter, ExecuteMsg, + find_threshold, AddressFunds, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, InstantiateMsg, QueryMsg, }; +use std::vec; use andromeda_std::{ ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, @@ -177,13 +176,12 @@ fn execute_send(ctx: ExecuteContext) -> Result { for (i, coin) in info.funds.clone().iter().enumerate() { let mut recip_coin: Coin = coin.clone(); - // Difference between the range's max and current funds received - let till_threshold = threshold.range.max - recipient_addr.funds; + // Difference between the next threshold's min and current funds received // If info.funds is greater than the below number, it means that the threshold will be surpassed. //TODO Multiply till_threshold with the current threshold's percentage, the additional funds (info.funds - till_threshold) will use the next threshold's percentage. - let funds_surpass_threshold = - till_threshold.checked_div_floor(recipient_percent).unwrap(); + // let funds_surpass_threshold = + // till_threshold.checked_div_floor(recipient_percent).unwrap(); // Save new amount sent recip_coin.amount = coin.amount * recipient_percent; diff --git a/contracts/finance/andromeda-conditional-splitter/src/state.rs b/contracts/finance/andromeda-conditional-splitter/src/state.rs index 1e03113c8..4f7360606 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/state.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/state.rs @@ -1,6 +1,6 @@ use andromeda_finance::conditional_splitter::ConditionalSplitter; -use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::{Item, Map}; +use cosmwasm_std::Addr; +use cw_storage_plus::Item; pub const CONDITIONAL_SPLITTER: Item = Item::new("conditional_splitter"); pub const KERNEL_ADDRESS: Item = Item::new("kernel_address"); diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index c8528a795..f6e91b47a 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -23,7 +23,7 @@ use crate::{ }; use andromeda_finance::conditional_splitter::{ AddressFunds, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, - InstantiateMsg, QueryMsg, Range, Threshold, + InstantiateMsg, QueryMsg, Threshold, }; fn init(deps: DepsMut) -> Response { @@ -37,11 +37,11 @@ fn init(deps: DepsMut) -> Response { recipients: vec![Recipient::from_string(String::from("some_address"))], thresholds: vec![ Threshold::new( - Range::new(Uint128::zero(), Uint128::new(10)), + Uint128::zero(), Decimal::from_ratio(Uint128::one(), Uint128::new(2)), ), Threshold::new( - Range::new(Uint128::new(11), Uint128::new(20)), + Uint128::new(11), Decimal::from_ratio(Uint128::one(), Uint128::new(2)), ), ], @@ -74,7 +74,7 @@ fn test_execute_update_lock() { recipients: vec![], lock: Milliseconds::from_seconds(current_time - 1), thresholds: vec![Threshold { - range: Range::new(Uint128::zero(), Uint128::new(10)), + min: Uint128::zero(), percentage: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), }], }; diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 32748375a..26254924e 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -8,54 +8,37 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ensure, Decimal, Deps, Uint128}; use std::collections::HashSet; -#[cw_serde] -// The range of received funds which will correspond to a certain percentage. -pub struct Range { - // Lower bound of the range - pub min: Uint128, - // Upper bound of the range - pub max: Uint128, -} -impl Range { - pub fn new(min: Uint128, max: Uint128) -> Self { - Self { min, max } - } - pub fn verify_range(&self) -> Result<(), ContractError> { - if self.min < self.max { - Ok(()) - } else { - Err(ContractError::InvalidRange {}) - } - } - pub fn contains(&self, num: Uint128) -> bool { - num >= self.min && num <= self.max - } -} - // The contract owner will input a vector of Threshold #[cw_serde] pub struct Threshold { - pub range: Range, + pub min: Uint128, pub percentage: Decimal, } impl Threshold { - pub fn new(range: Range, percentage: Decimal) -> Self { - Self { range, percentage } + pub fn new(min: Uint128, percentage: Decimal) -> Self { + Self { min, percentage } } - pub fn contains(&self, num: Uint128) -> bool { - self.range.contains(num) + pub fn in_range(&self, num: Uint128) -> bool { + num >= self.min } } -pub fn find_threshold(thresholds: &[Threshold], num: Uint128) -> Result<&Threshold, ContractError> { - let threshold = thresholds.iter().find(|&threshold| threshold.contains(num)); - if let Some(threshold) = threshold { - Ok(threshold) - } else { - Err(ContractError::InvalidRange {}) +pub fn find_threshold(thresholds: &[Threshold], num: Uint128) -> Result { + let mut sorted_thresholds = thresholds.to_vec(); + + // Sort thresholds by min values in decreasing order + sorted_thresholds.sort_by(|a, b| b.min.cmp(&a.min)); + + // Return the first threshold where the number is in its range + for threshold in sorted_thresholds { + if threshold.in_range(num) { + return Ok(threshold); + } } + Err(ContractError::InvalidRange {}) } +/// Used to couple each recipient with the funds he's received so far #[cw_serde] pub struct AddressFunds { pub recipient: Recipient, @@ -156,35 +139,28 @@ pub fn validate_recipient_list( Ok(()) } - +/// Makes sure the percentages don't exceed 100 and that there are no duplicate min values pub fn validate_thresholds( thresholds: &Vec, recipients: &Vec, ) -> Result<(), ContractError> { let number_of_recipients = recipients.len() as u128; - let mut prev_max: Option = None; for threshold in thresholds { - // Check that the range is valid (min > max) - threshold.range.verify_range()?; - // The percentage multiplied by the number of recipients shouldn't exceed 100 ensure!( threshold.percentage * Uint128::new(number_of_recipients) <= Uint128::new(100), ContractError::AmountExceededHundredPrecent {} ); - - // The ranges shouldn't overlap - if let Some(max) = prev_max { - if threshold.range.min <= max { - return Err(ContractError::OverlappingRanges {}); - } - } - - // Update prev_max - prev_max = Some(threshold.range.max); } + // Check that there are no duplicate minimum values + let min_values: HashSet<_> = thresholds.iter().map(|t| t.min.u128()).collect(); + ensure!( + min_values.len() == thresholds.len(), + ContractError::DuplicateThresholds {} + ); + Ok(()) } diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index b13ad2f50..aab6f9db7 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -395,6 +395,9 @@ pub enum ContractError { #[error("DuplicateCoinDenoms")] DuplicateCoinDenoms {}, + #[error("DuplicateThresholds")] + DuplicateThresholds {}, + #[error("DuplicateRecipient")] DuplicateRecipient {}, From 7a1f40ef874a975b38bd31792d76a19e5a239f5d Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 24 Apr 2024 15:52:41 +0300 Subject: [PATCH 03/28] test: execute_send with two thresholds and two recipients --- .../src/contract.rs | 5 +- .../src/testing/tests.rs | 176 +++++++++++------- 2 files changed, 108 insertions(+), 73 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 7352b93ce..77fc51422 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -183,14 +183,13 @@ fn execute_send(ctx: ExecuteContext) -> Result { // let funds_surpass_threshold = // till_threshold.checked_div_floor(recipient_percent).unwrap(); - // Save new amount sent recip_coin.amount = coin.amount * recipient_percent; // Save new funds - let new_fund = recipient_addr.funds + recip_coin.amount; + let new_funds = recipient_addr.funds + recip_coin.amount; let new_address_funds = AddressFunds { recipient: recipient_addr.recipient.clone(), - funds: new_fund, + funds: new_funds, }; recipients_with_new_funds.push(new_address_funds); diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index f6e91b47a..5fd87ce62 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -172,87 +172,123 @@ fn test_execute_update_lock() { // assert_eq!(splitter.recipients, recipients); // } -// #[test] -// fn test_execute_send() { -// let mut deps = mock_dependencies_custom(&[]); -// let env = mock_env(); -// let _res: Response = init(deps.as_mut()); +#[test] +fn test_execute_send() { + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); -// let sender_funds_amount = 10000u128; + let recip_address1 = "address1".to_string(); + let recip_address2 = "address2".to_string(); -// let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); + let second_threshold = Uint128::new(11); -// let recip_address1 = "address1".to_string(); -// let recip_percent1 = 10; // 10% - -// let recip_address2 = "address2".to_string(); -// let recip_percent2 = 20; // 20% + let msg = InstantiateMsg { + owner: Some(OWNER.to_owned()), + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + recipients: vec![ + Recipient::from_string(recip_address1.clone()), + Recipient::from_string(recip_address2.clone()), + ], + thresholds: vec![ + Threshold::new( + Uint128::zero(), + // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + Threshold::new( + second_threshold, + // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + ], + lock_time: Some(Milliseconds::from_seconds(100_000)), + }; -// let recip1 = Recipient::from_string(recip_address1); -// let recip2 = Recipient::from_string(recip_address2); + let info = mock_info("owner", &[]); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); -// let recipient = vec![ -// AddressFunds { -// recipient: recip1.clone(), -// percent: Decimal::percent(recip_percent1), -// }, -// AddressFunds { -// recipient: recip2.clone(), -// percent: Decimal::percent(recip_percent2), -// }, -// ]; -// let msg = ExecuteMsg::Send {}; + // First batch of funds which will put funds received to 5 for each recipient since the percentage is 50 + let first_batch = 10u128; -// let amp_msg_1 = recip1 -// .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(1000, "uluna")])) -// .unwrap(); -// let amp_msg_2 = recip2 -// .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(2000, "uluna")])) -// .unwrap(); -// let amp_pkt = AMPPkt::new( -// MOCK_CONTRACT_ADDR.to_string(), -// MOCK_CONTRACT_ADDR.to_string(), -// vec![amp_msg_1, amp_msg_2], -// ); -// let amp_msg = amp_pkt -// .to_sub_msg( -// MOCK_KERNEL_CONTRACT, -// Some(vec![Coin::new(1000, "uluna"), Coin::new(2000, "uluna")]), -// 1, -// ) -// .unwrap(); + // Second batch of funds will push funds sent to 11 (5 + (12/2)) which is the min value of second batch + let second_batch = 12u128; -// let splitter = ConditionalSplitter { -// recipients: recipient, -// lock: Milliseconds::default(), -// thresholds: vec![Threshold { -// range: Range::new(Uint128::zero(), Uint128::new(10)), -// percentage: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), -// }], -// }; + // Third batch will be used to test the second threshold + let third_batch = 100u128; -// CONDITIONAL_SPLITTER -// .save(deps.as_mut().storage, &splitter) -// .unwrap(); + let recip1 = Recipient::from_string(recip_address1); + let recip2 = Recipient::from_string(recip_address2); -// let res = execute(deps.as_mut(), env, info, msg).unwrap(); + // First batch + let info = mock_info(OWNER, &[Coin::new(first_batch, "uandr")]); + let msg = ExecuteMsg::Send {}; + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); -// let expected_res = Response::new() -// .add_submessages(vec![ -// SubMsg::new( -// // refunds remainder to sender -// CosmosMsg::Bank(BankMsg::Send { -// to_address: OWNER.to_string(), -// amount: vec![Coin::new(7000, "uluna")], // 10000 * 0.7 remainder -// }), -// ), -// amp_msg, -// ]) -// .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) -// .add_submessage(generate_economics_message(OWNER, "Send")); + let address_funds = CONDITIONAL_SPLITTER.load(&deps.storage).unwrap().recipients; + let expected_address_funds = vec![ + AddressFunds { + recipient: recip1.clone(), + funds: Uint128::new(5), + }, + AddressFunds { + recipient: recip2.clone(), + funds: Uint128::new(5), + }, + ]; + assert_eq!(address_funds, expected_address_funds); + + // Second batch + let info = mock_info(OWNER, &[Coin::new(second_batch, "uandr")]); + let msg = ExecuteMsg::Send {}; + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); -// assert_eq!(res, expected_res); -// } + let address_funds = CONDITIONAL_SPLITTER.load(&deps.storage).unwrap().recipients; + let expected_address_funds = vec![ + AddressFunds { + recipient: recip1.clone(), + funds: Uint128::new(5 + 6), + }, + AddressFunds { + recipient: recip2.clone(), + funds: Uint128::new(5 + 6), + }, + ]; + assert_eq!(address_funds, expected_address_funds); + + // Third batch + let info = mock_info(OWNER, &[Coin::new(third_batch, "uandr")]); + let msg = ExecuteMsg::Send {}; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + + let address_funds = CONDITIONAL_SPLITTER.load(&deps.storage).unwrap().recipients; + let expected_address_funds = vec![ + AddressFunds { + recipient: recip1.clone(), + funds: Uint128::new(5 + 6 + 20), + }, + AddressFunds { + recipient: recip2.clone(), + funds: Uint128::new(5 + 6 + 20), + }, + ]; + assert_eq!(address_funds, expected_address_funds); + + // let expected_res = Response::new() + // .add_submessages(vec![ + // SubMsg::new( + // // refunds remainder to sender + // CosmosMsg::Bank(BankMsg::Send { + // to_address: OWNER.to_string(), + // amount: vec![Coin::new(7000, "uluna")], // 10000 * 0.7 remainder + // }), + // ), + // amp_msg, + // ]) + // .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) + // .add_submessage(generate_economics_message(OWNER, "Send")); + + // assert_eq!(res, expected_res); +} // #[test] // fn test_execute_send_ado_recipient() { From 20257490ff299ecf05b8a2e1b233af47a6006d75 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 24 Apr 2024 17:32:34 +0300 Subject: [PATCH 04/28] refactor: funds sent refer to overall contract, different percentages for the recipients --- .../src/contract.rs | 39 ++----- .../src/state.rs | 3 +- .../src/testing/tests.rs | 104 +++++++----------- .../src/conditional_splitter.rs | 92 ++++++++++------ packages/std/src/error.rs | 3 + 5 files changed, 114 insertions(+), 127 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 77fc51422..8ee53ec58 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -1,7 +1,7 @@ -use crate::state::CONDITIONAL_SPLITTER; +use crate::state::{CONDITIONAL_SPLITTER, FUNDS_DISTRIBUTED}; use andromeda_finance::conditional_splitter::{ - find_threshold, AddressFunds, ConditionalSplitter, ExecuteMsg, - GetConditionalSplitterConfigResponse, InstantiateMsg, QueryMsg, + find_threshold, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, + InstantiateMsg, QueryMsg, }; use std::vec; @@ -35,14 +35,8 @@ pub fn instantiate( ) -> Result { let current_time = Milliseconds::from_seconds(env.block.time.seconds()); - // Construct Address Fund, funds automatically set to 0 - let mut address_funds: Vec = vec![]; - for recipient in msg.recipients { - address_funds.push(AddressFunds::new(recipient)) - } - let mut conditional_splitter = ConditionalSplitter { - recipients: address_funds, + recipients: msg.recipients, thresholds: msg.thresholds.clone(), lock: msg.lock_time.unwrap_or_default(), }; @@ -69,6 +63,7 @@ pub fn instantiate( } // Save kernel address after validating it CONDITIONAL_SPLITTER.save(deps.storage, &conditional_splitter)?; + FUNDS_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; let inst_resp = ADOContract::default().instantiate( deps.storage, @@ -165,12 +160,14 @@ fn execute_send(ctx: ExecuteContext) -> Result { let mut remainder_funds = info.funds.clone(); let mut pkt = AMPPkt::from_ctx(ctx.amp_ctx, ctx.env.contract.address.to_string()); - let mut recipients_with_new_funds: Vec = vec![]; + + let funds_distributed = FUNDS_DISTRIBUTED.load(deps.storage)?; for recipient_addr in &conditional_splitter.recipients { // Get current range - let threshold = find_threshold(&conditional_splitter.thresholds, recipient_addr.funds)?; - let recipient_percent = threshold.percentage; + let (_threshold, threshold_index) = + find_threshold(&conditional_splitter.thresholds, funds_distributed)?; + let recipient_percent = recipient_addr.percentages[threshold_index]; let mut vec_coin: Vec = Vec::new(); for (i, coin) in info.funds.clone().iter().enumerate() { @@ -185,14 +182,6 @@ fn execute_send(ctx: ExecuteContext) -> Result { recip_coin.amount = coin.amount * recipient_percent; - // Save new funds - let new_funds = recipient_addr.funds + recip_coin.amount; - let new_address_funds = AddressFunds { - recipient: recipient_addr.recipient.clone(), - funds: new_funds, - }; - recipients_with_new_funds.push(new_address_funds); - remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; vec_coin.push(recip_coin.clone()); amp_funds.push(recip_coin); @@ -203,12 +192,8 @@ fn execute_send(ctx: ExecuteContext) -> Result { .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; pkt = pkt.add_message(amp_msg); } - let new_conditional_splitter = ConditionalSplitter { - recipients: recipients_with_new_funds, - thresholds: conditional_splitter.thresholds, - lock: conditional_splitter.lock, - }; - CONDITIONAL_SPLITTER.save(deps.storage, &new_conditional_splitter)?; + let new_funds_distributed = info.funds.first().unwrap().amount + funds_distributed; + FUNDS_DISTRIBUTED.save(deps.storage, &new_funds_distributed)?; remainder_funds.retain(|x| x.amount > Uint128::zero()); diff --git a/contracts/finance/andromeda-conditional-splitter/src/state.rs b/contracts/finance/andromeda-conditional-splitter/src/state.rs index 4f7360606..c5e95a7f0 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/state.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/state.rs @@ -1,6 +1,7 @@ use andromeda_finance::conditional_splitter::ConditionalSplitter; -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::Item; pub const CONDITIONAL_SPLITTER: Item = Item::new("conditional_splitter"); pub const KERNEL_ADDRESS: Item = Item::new("kernel_address"); +pub const FUNDS_DISTRIBUTED: Item = Item::new("funds_distributed"); diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 5fd87ce62..42e02378d 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -18,32 +18,28 @@ use super::mock_querier::MOCK_KERNEL_CONTRACT; use crate::{ contract::{execute, instantiate, query}, - state::CONDITIONAL_SPLITTER, + state::{CONDITIONAL_SPLITTER, FUNDS_DISTRIBUTED}, testing::mock_querier::mock_dependencies_custom, }; use andromeda_finance::conditional_splitter::{ - AddressFunds, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, + AddressPercentages, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, InstantiateMsg, QueryMsg, Threshold, }; fn init(deps: DepsMut) -> Response { - let mock_recipient: Vec = vec![AddressFunds { - recipient: Recipient::from_string(String::from("some_address")), - funds: Uint128::zero(), - }]; let msg = InstantiateMsg { owner: Some(OWNER.to_owned()), kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - recipients: vec![Recipient::from_string(String::from("some_address"))], - thresholds: vec![ - Threshold::new( - Uint128::zero(), + recipients: vec![AddressPercentages::new( + Recipient::from_string(String::from("some_address")), + vec![ Decimal::from_ratio(Uint128::one(), Uint128::new(2)), - ), - Threshold::new( - Uint128::new(11), Decimal::from_ratio(Uint128::one(), Uint128::new(2)), - ), + ], + )], + thresholds: vec![ + Threshold::new(Uint128::zero()), + Threshold::new(Uint128::new(11)), ], lock_time: Some(Milliseconds::from_seconds(100_000)), }; @@ -75,7 +71,6 @@ fn test_execute_update_lock() { lock: Milliseconds::from_seconds(current_time - 1), thresholds: vec![Threshold { min: Uint128::zero(), - percentage: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), }], }; @@ -182,25 +177,32 @@ fn test_execute_send() { let second_threshold = Uint128::new(11); + let recip1 = Recipient::from_string(recip_address1); + let recip2 = Recipient::from_string(recip_address2); + let msg = InstantiateMsg { owner: Some(OWNER.to_owned()), kernel_address: MOCK_KERNEL_CONTRACT.to_string(), recipients: vec![ - Recipient::from_string(recip_address1.clone()), - Recipient::from_string(recip_address2.clone()), - ], - thresholds: vec![ - Threshold::new( - Uint128::zero(), - // 50% - Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + AddressPercentages::new( + recip1, + vec![ + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ], ), - Threshold::new( - second_threshold, - // 20% - Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + AddressPercentages::new( + recip2, + vec![ + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + ], ), ], + thresholds: vec![ + Threshold::new(Uint128::zero()), + Threshold::new(second_threshold), + ], lock_time: Some(Milliseconds::from_seconds(100_000)), }; @@ -216,62 +218,32 @@ fn test_execute_send() { // Third batch will be used to test the second threshold let third_batch = 100u128; - let recip1 = Recipient::from_string(recip_address1); - let recip2 = Recipient::from_string(recip_address2); - // First batch let info = mock_info(OWNER, &[Coin::new(first_batch, "uandr")]); let msg = ExecuteMsg::Send {}; let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - let address_funds = CONDITIONAL_SPLITTER.load(&deps.storage).unwrap().recipients; - let expected_address_funds = vec![ - AddressFunds { - recipient: recip1.clone(), - funds: Uint128::new(5), - }, - AddressFunds { - recipient: recip2.clone(), - funds: Uint128::new(5), - }, - ]; - assert_eq!(address_funds, expected_address_funds); + let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); + let expected_funds_distributed = Uint128::new(10); + assert_eq!(funds_distributed, expected_funds_distributed); // Second batch let info = mock_info(OWNER, &[Coin::new(second_batch, "uandr")]); let msg = ExecuteMsg::Send {}; let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - let address_funds = CONDITIONAL_SPLITTER.load(&deps.storage).unwrap().recipients; - let expected_address_funds = vec![ - AddressFunds { - recipient: recip1.clone(), - funds: Uint128::new(5 + 6), - }, - AddressFunds { - recipient: recip2.clone(), - funds: Uint128::new(5 + 6), - }, - ]; - assert_eq!(address_funds, expected_address_funds); + let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); + let expected_funds_distributed = Uint128::new(10 + 12); + assert_eq!(funds_distributed, expected_funds_distributed); // Third batch let info = mock_info(OWNER, &[Coin::new(third_batch, "uandr")]); let msg = ExecuteMsg::Send {}; let res = execute(deps.as_mut(), env, info, msg).unwrap(); - let address_funds = CONDITIONAL_SPLITTER.load(&deps.storage).unwrap().recipients; - let expected_address_funds = vec![ - AddressFunds { - recipient: recip1.clone(), - funds: Uint128::new(5 + 6 + 20), - }, - AddressFunds { - recipient: recip2.clone(), - funds: Uint128::new(5 + 6 + 20), - }, - ]; - assert_eq!(address_funds, expected_address_funds); + let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); + let expected_funds_distributed = Uint128::new(10 + 12 + 100); + assert_eq!(funds_distributed, expected_funds_distributed); // let expected_res = Response::new() // .add_submessages(vec![ diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 26254924e..379a4c7ea 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -12,43 +12,51 @@ use std::collections::HashSet; #[cw_serde] pub struct Threshold { pub min: Uint128, - pub percentage: Decimal, } impl Threshold { - pub fn new(min: Uint128, percentage: Decimal) -> Self { - Self { min, percentage } + pub fn new(min: Uint128) -> Self { + Self { min } } pub fn in_range(&self, num: Uint128) -> bool { num >= self.min } } -pub fn find_threshold(thresholds: &[Threshold], num: Uint128) -> Result { - let mut sorted_thresholds = thresholds.to_vec(); +pub fn find_threshold( + thresholds: &[Threshold], + num: Uint128, +) -> Result<(Threshold, usize), ContractError> { + // Create a vector of tuples containing the original index and the threshold + let mut indexed_thresholds: Vec<(usize, &Threshold)> = thresholds.iter().enumerate().collect(); // Sort thresholds by min values in decreasing order - sorted_thresholds.sort_by(|a, b| b.min.cmp(&a.min)); + indexed_thresholds.sort_by(|a, b| b.1.min.cmp(&a.1.min)); - // Return the first threshold where the number is in its range - for threshold in sorted_thresholds { + // Iterate over the sorted indexed thresholds + for (index, threshold) in indexed_thresholds { if threshold.in_range(num) { - return Ok(threshold); + // Get original index + let original_index = thresholds.len() - 1 - index; + // Return the threshold and its original index + return Ok((threshold.clone(), original_index)); } } Err(ContractError::InvalidRange {}) } -/// Used to couple each recipient with the funds he's received so far #[cw_serde] -pub struct AddressFunds { +pub struct AddressPercentages { pub recipient: Recipient, - pub funds: Uint128, + // The sequence of the the percentages should correspond to each threshold. + // For example the first value in percentages should correspond to the first threshold + pub percentages: Vec, } -impl AddressFunds { - pub fn new(recipient: Recipient) -> Self { + +impl AddressPercentages { + pub fn new(recipient: Recipient, percentages: Vec) -> Self { Self { recipient, - funds: Uint128::zero(), + percentages, } } } @@ -57,7 +65,7 @@ impl AddressFunds { /// A config struct for a `Conditional Splitter` contract. pub struct ConditionalSplitter { /// The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on the threshold. - pub recipients: Vec, + pub recipients: Vec, /// The vector of thresholds which assign a percentage for a certain range of received funds pub thresholds: Vec, /// The lock's expiration time @@ -75,7 +83,7 @@ impl ConditionalSplitter { pub struct InstantiateMsg { /// The vector of recipients for the contract. Anytime a `Send` execute message is /// sent the amount sent will be divided amongst these recipients depending on their assigned percentage. - pub recipients: Vec, + pub recipients: Vec, pub thresholds: Vec, pub lock_time: Option, } @@ -109,13 +117,13 @@ pub struct GetConditionalSplitterConfigResponse { } /// Ensures that a given list of recipients for a `splitter` contract is valid: -/// +/// * Percentages of corresponding indexes should not sum up to over 100 /// * Must include at least one recipient /// * The number of recipients must not exceed 100 /// * The recipient addresses must be unique pub fn validate_recipient_list( deps: Deps, - recipients: Vec, + recipients: Vec, ) -> Result<(), ContractError> { ensure!( !recipients.is_empty(), @@ -125,32 +133,50 @@ pub fn validate_recipient_list( recipients.len() <= 100, ContractError::ReachedRecipientLimit {} ); - let mut recipient_address_set = HashSet::new(); - for recipient in recipients { - recipient.recipient.validate(&deps)?; - let recipient_address = recipient.recipient.address.get_raw_address(&deps)?; + for i in 0..recipients[0].percentages.len() { + // Collect the ith percentage of each recipient + let mut i_percentages = Decimal::zero(); + let mut recipient_address_set = HashSet::new(); + for recipient in &recipients { + // Check for invalid percentages + i_percentages = i_percentages.checked_add(recipient.percentages[i])?; + + // Checks for duplicate and invalid recipients + recipient.recipient.validate(&deps)?; + let recipient_address = recipient.recipient.address.get_raw_address(&deps)?; + ensure!( + !recipient_address_set.contains(&recipient_address), + ContractError::DuplicateRecipient {} + ); + recipient_address_set.insert(recipient_address); + } ensure!( - !recipient_address_set.contains(&recipient_address), - ContractError::DuplicateRecipient {} + i_percentages <= Decimal::one(), + ContractError::AmountExceededHundredPrecent {} ); - recipient_address_set.insert(recipient_address); } - Ok(()) } + /// Makes sure the percentages don't exceed 100 and that there are no duplicate min values pub fn validate_thresholds( thresholds: &Vec, - recipients: &Vec, + recipients: &Vec, ) -> Result<(), ContractError> { - let number_of_recipients = recipients.len() as u128; + let number_of_thresholds = thresholds.len(); - for threshold in thresholds { - // The percentage multiplied by the number of recipients shouldn't exceed 100 + // Check that each recipient has the same amount of percentages as thresholds + for recipient in recipients { ensure!( - threshold.percentage * Uint128::new(number_of_recipients) <= Uint128::new(100), - ContractError::AmountExceededHundredPrecent {} + recipient.percentages.len() == number_of_thresholds, + ContractError::ThresholdsPercentagesDiscrepancy { + msg: format!( + "The number of thresholds is: {:?}, whereas the numer of percentages is: {:?}", + number_of_thresholds, + recipient.percentages.len() + ) + } ); } diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index aab6f9db7..6d77e6240 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -152,6 +152,9 @@ pub enum ContractError { #[error("InsufficientBondedTime")] InsufficientBondedTime {}, + #[error("ThresholdsPercentagesDiscrepancy: {msg}")] + ThresholdsPercentagesDiscrepancy { msg: String }, + #[error("LockTimeTooShort")] LockTimeTooShort {}, From 531c2fb66dacb3b785c958f32fd447a5c124a50d Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 24 Apr 2024 17:56:37 +0300 Subject: [PATCH 05/28] test: execute_send adjusted for previous refactor --- .../src/testing/tests.rs | 94 +++++++++++++++++-- .../src/conditional_splitter.rs | 4 +- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 42e02378d..bc33eb196 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -175,7 +175,7 @@ fn test_execute_send() { let recip_address1 = "address1".to_string(); let recip_address2 = "address2".to_string(); - let second_threshold = Uint128::new(11); + let second_threshold = Uint128::new(10); let recip1 = Recipient::from_string(recip_address1); let recip2 = Recipient::from_string(recip_address2); @@ -185,16 +185,20 @@ fn test_execute_send() { kernel_address: MOCK_KERNEL_CONTRACT.to_string(), recipients: vec![ AddressPercentages::new( - recip1, + recip1.clone(), vec![ + // 50% Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + // 20% Decimal::from_ratio(Uint128::one(), Uint128::new(5)), ], ), AddressPercentages::new( - recip2, + recip2.clone(), vec![ + // 20% Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + // 10% Decimal::from_ratio(Uint128::one(), Uint128::new(10)), ], ), @@ -209,11 +213,11 @@ fn test_execute_send() { let info = mock_info("owner", &[]); instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - // First batch of funds which will put funds received to 5 for each recipient since the percentage is 50 + // First batch of funds which will put funds received to 10 and push the next batch to the other threshold let first_batch = 10u128; - // Second batch of funds will push funds sent to 11 (5 + (12/2)) which is the min value of second batch - let second_batch = 12u128; + // Second batch will be used to test the second threshold + let second_batch = 10u128; // Third batch will be used to test the second threshold let third_batch = 100u128; @@ -223,6 +227,43 @@ fn test_execute_send() { let msg = ExecuteMsg::Send {}; let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + // 50 percent + let amp_msg_1 = recip1 + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(5, "uandr")])) + .unwrap(); + // 20 percent + let amp_msg_2 = recip2 + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(2, "uandr")])) + .unwrap(); + let amp_pkt = AMPPkt::new( + MOCK_CONTRACT_ADDR.to_string(), + MOCK_CONTRACT_ADDR.to_string(), + vec![amp_msg_1, amp_msg_2], + ); + let amp_msg = amp_pkt + .to_sub_msg( + MOCK_KERNEL_CONTRACT, + Some(vec![Coin::new(5, "uandr"), Coin::new(2, "uandr")]), + 1, + ) + .unwrap(); + + let expected_res = Response::new() + .add_submessages(vec![ + SubMsg::new( + // refunds remainder to sender + CosmosMsg::Bank(BankMsg::Send { + to_address: OWNER.to_string(), + amount: vec![Coin::new(3, "uandr")], // 10 - (0.5 * 10) - (0.2 * 10) remainder + }), + ), + amp_msg, + ]) + .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) + .add_submessage(generate_economics_message(OWNER, "Send")); + + assert_eq!(res, expected_res); + let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); let expected_funds_distributed = Uint128::new(10); assert_eq!(funds_distributed, expected_funds_distributed); @@ -233,16 +274,53 @@ fn test_execute_send() { let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); - let expected_funds_distributed = Uint128::new(10 + 12); + let expected_funds_distributed = Uint128::new(10 + 10); assert_eq!(funds_distributed, expected_funds_distributed); + // 20 percent + let amp_msg_1 = recip1 + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(2, "uandr")])) + .unwrap(); + // 10 percent + let amp_msg_2 = recip2 + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(1, "uandr")])) + .unwrap(); + let amp_pkt = AMPPkt::new( + MOCK_CONTRACT_ADDR.to_string(), + MOCK_CONTRACT_ADDR.to_string(), + vec![amp_msg_1, amp_msg_2], + ); + let amp_msg = amp_pkt + .to_sub_msg( + MOCK_KERNEL_CONTRACT, + Some(vec![Coin::new(2, "uandr"), Coin::new(1, "uandr")]), + 1, + ) + .unwrap(); + + let expected_res = Response::new() + .add_submessages(vec![ + SubMsg::new( + // refunds remainder to sender + CosmosMsg::Bank(BankMsg::Send { + to_address: OWNER.to_string(), + amount: vec![Coin::new(7, "uandr")], // 10 - (0.2 * 10) - (0.1 * 10) remainder + }), + ), + amp_msg, + ]) + .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) + .add_submessage(generate_economics_message(OWNER, "Send")); + + assert_eq!(res, expected_res); + // Third batch let info = mock_info(OWNER, &[Coin::new(third_batch, "uandr")]); let msg = ExecuteMsg::Send {}; let res = execute(deps.as_mut(), env, info, msg).unwrap(); let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); - let expected_funds_distributed = Uint128::new(10 + 12 + 100); + let expected_funds_distributed = Uint128::new(10 + 10 + 100); assert_eq!(funds_distributed, expected_funds_distributed); // let expected_res = Response::new() diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 379a4c7ea..df5a596bf 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -35,10 +35,8 @@ pub fn find_threshold( // Iterate over the sorted indexed thresholds for (index, threshold) in indexed_thresholds { if threshold.in_range(num) { - // Get original index - let original_index = thresholds.len() - 1 - index; // Return the threshold and its original index - return Ok((threshold.clone(), original_index)); + return Ok((threshold.clone(), index)); } } Err(ContractError::InvalidRange {}) From d53ac11e3385a4177a476b15580c0fa0cae83c86 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 25 Apr 2024 09:22:26 +0300 Subject: [PATCH 06/28] test: uncommented some unit tests --- .../src/contract.rs | 2 +- .../src/testing/tests.rs | 205 +++++++++--------- .../src/conditional_splitter.rs | 2 +- 3 files changed, 106 insertions(+), 103 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 8ee53ec58..e0961c740 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -313,7 +313,7 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { match msg { - QueryMsg::GetSplitterConfig {} => encode_binary(&query_splitter(deps)?), + QueryMsg::GetConditionalSplitterConfig {} => encode_binary(&query_splitter(deps)?), _ => ADOContract::default().query(deps, env, msg), } } diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index bc33eb196..2f3448227 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -110,6 +110,7 @@ fn test_execute_update_lock() { // let splitter = ConditionalSplitter { // recipients: vec![], // lock: Milliseconds::from_seconds(0), +// thresholds: vec![Threshold::new(Uint128::zero())], // }; // CONDITIONAL_SPLITTER @@ -118,13 +119,13 @@ fn test_execute_update_lock() { // // Duplicate recipients // let duplicate_recipients = vec![ -// AddressFunds { +// AddressPercentages { // recipient: Recipient::from_string(String::from("addr1")), -// percent: Decimal::percent(40), +// percentages: vec![Decimal::percent(40)], // }, -// AddressFunds { +// AddressPercentages { // recipient: Recipient::from_string(String::from("addr1")), -// percent: Decimal::percent(60), +// percentages: vec![Decimal::percent(60)], // }, // ]; // let msg = ExecuteMsg::UpdateRecipients { @@ -418,79 +419,81 @@ fn test_execute_send() { // assert_eq!(res, expected_res); // } -// #[test] -// fn test_handle_packet_exit_with_error_true() { -// let mut deps = mock_dependencies_custom(&[]); -// let env = mock_env(); -// let _res: Response = init(deps.as_mut()); - -// let sender_funds_amount = 0u128; -// let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); - -// let recip_address1 = "address1".to_string(); -// let recip_percent1 = 10; // 10% +#[test] +fn test_handle_packet_exit_with_error_true() { + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); + let _res: Response = init(deps.as_mut()); -// let recip_percent2 = 20; // 20% + let sender_funds_amount = 0u128; + let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); -// let recipient = vec![ -// AddressFunds { -// recipient: Recipient::from_string(recip_address1.clone()), -// percent: Decimal::percent(recip_percent1), -// }, -// AddressFunds { -// recipient: Recipient::from_string(recip_address1.clone()), -// percent: Decimal::percent(recip_percent2), -// }, -// ]; -// let pkt = AMPPkt::new( -// info.clone().sender, -// "cosmos2contract", -// vec![AMPMsg::new( -// recip_address1, -// to_json_binary(&ExecuteMsg::Send {}).unwrap(), -// Some(vec![Coin::new(0, "uluna")]), -// )], -// ); -// let msg = ExecuteMsg::AMPReceive(pkt); + let recip_address1 = "address1".to_string(); + let recip_percent1 = 10; // 10% + + let recip_percent2 = 20; // 20% + + let recipient = vec![ + AddressPercentages { + recipient: Recipient::from_string(recip_address1.clone()), + percentages: vec![Decimal::percent(recip_percent1)], + }, + AddressPercentages { + recipient: Recipient::from_string(recip_address1.clone()), + percentages: vec![Decimal::percent(recip_percent2)], + }, + ]; + let pkt = AMPPkt::new( + info.clone().sender, + "cosmos2contract", + vec![AMPMsg::new( + recip_address1, + to_json_binary(&ExecuteMsg::Send {}).unwrap(), + Some(vec![Coin::new(0, "uluna")]), + )], + ); + let msg = ExecuteMsg::AMPReceive(pkt); -// let splitter = ConditionalSplitter { -// recipients: recipient, -// lock: Milliseconds::default(), -// }; + let splitter = ConditionalSplitter { + recipients: recipient, + lock: Milliseconds::default(), + thresholds: vec![Threshold::new(Uint128::zero())], + }; -// CONDITIONAL_SPLITTER -// .save(deps.as_mut().storage, &splitter) -// .unwrap(); + CONDITIONAL_SPLITTER + .save(deps.as_mut().storage, &splitter) + .unwrap(); -// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); -// assert_eq!( -// err, -// ContractError::InvalidFunds { -// msg: "Amount must be non-zero".to_string(), -// } -// ); -// } + assert_eq!( + err, + ContractError::InvalidFunds { + msg: "Amount must be non-zero".to_string(), + } + ); +} -// #[test] -// fn test_query_splitter() { -// let mut deps = mock_dependencies_custom(&[]); -// let env = mock_env(); -// let splitter = ConditionalSplitter { -// recipients: vec![], -// lock: Milliseconds::default(), -// }; +#[test] +fn test_query_splitter() { + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); + let splitter = ConditionalSplitter { + recipients: vec![], + lock: Milliseconds::default(), + thresholds: vec![Threshold::new(Uint128::zero())], + }; -// CONDITIONAL_SPLITTER -// .save(deps.as_mut().storage, &splitter) -// .unwrap(); + CONDITIONAL_SPLITTER + .save(deps.as_mut().storage, &splitter) + .unwrap(); -// let query_msg = QueryMsg::GetConditionalSplitterConfig {}; -// let res = query(deps.as_ref(), env, query_msg).unwrap(); -// let val: GetConditionalSplitterConfigResponse = from_json(res).unwrap(); + let query_msg = QueryMsg::GetConditionalSplitterConfig {}; + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let val: GetConditionalSplitterConfigResponse = from_json(res).unwrap(); -// assert_eq!(val.config, splitter); -// } + assert_eq!(val.config, splitter); +} // #[test] // fn test_execute_send_error() { @@ -547,46 +550,46 @@ fn test_execute_send() { // assert_eq!(res, expected_res); // } -// #[test] -// fn test_update_app_contract() { -// let mut deps = mock_dependencies_custom(&[]); -// let _res: Response = init(deps.as_mut()); +#[test] +fn test_update_app_contract() { + let mut deps = mock_dependencies_custom(&[]); + let _res: Response = init(deps.as_mut()); -// let info = mock_info(OWNER, &[]); + let info = mock_info(OWNER, &[]); -// let msg = ExecuteMsg::UpdateAppContract { -// address: "app_contract".to_string(), -// }; + let msg = ExecuteMsg::UpdateAppContract { + address: "app_contract".to_string(), + }; -// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); -// assert_eq!( -// Response::new() -// .add_attribute("action", "update_app_contract") -// .add_attribute("address", "app_contract") -// .add_submessage(generate_economics_message(OWNER, "UpdateAppContract")), -// res -// ); -// } + assert_eq!( + Response::new() + .add_attribute("action", "update_app_contract") + .add_attribute("address", "app_contract") + .add_submessage(generate_economics_message(OWNER, "UpdateAppContract")), + res + ); +} -// #[test] -// fn test_update_app_contract_invalid_recipient() { -// let mut deps = mock_dependencies_custom(&[]); -// let _res: Response = init(deps.as_mut()); +#[test] +fn test_update_app_contract_invalid_recipient() { + let mut deps = mock_dependencies_custom(&[]); + let _res: Response = init(deps.as_mut()); -// let info = mock_info(OWNER, &[]); + let info = mock_info(OWNER, &[]); -// let msg = ExecuteMsg::UpdateAppContract { -// address: "z".to_string(), -// }; + let msg = ExecuteMsg::UpdateAppContract { + address: "z".to_string(), + }; -// let res = execute(deps.as_mut(), mock_env(), info, msg); + let res = execute(deps.as_mut(), mock_env(), info, msg); -// // assert_eq!( -// // ContractError::InvalidComponent { -// // name: "z".to_string() -// // }, -// // res.unwrap_err() -// // ); -// assert!(res.is_err()) -// } + // assert_eq!( + // ContractError::InvalidComponent { + // name: "z".to_string() + // }, + // res.unwrap_err() + // ); + assert!(res.is_err()) +} diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index df5a596bf..146a7a896 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -106,7 +106,7 @@ pub enum ExecuteMsg { pub enum QueryMsg { /// The current config of the Splitter contract #[returns(GetConditionalSplitterConfigResponse)] - GetSplitterConfig {}, + GetConditionalSplitterConfig {}, } #[cw_serde] From c369c4d0c5235a315f3ce331b0cbe212f26c278d Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 25 Apr 2024 14:05:22 +0300 Subject: [PATCH 07/28] feat: cross from one threshold to the next in one transaction --- .../src/contract.rs | 40 +++++++++++------ .../src/testing/tests.rs | 44 ++++++++++++++++++- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index e0961c740..e68fdb4f1 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -173,14 +173,33 @@ fn execute_send(ctx: ExecuteContext) -> Result { for (i, coin) in info.funds.clone().iter().enumerate() { let mut recip_coin: Coin = coin.clone(); - // Difference between the next threshold's min and current funds received - - // If info.funds is greater than the below number, it means that the threshold will be surpassed. - //TODO Multiply till_threshold with the current threshold's percentage, the additional funds (info.funds - till_threshold) will use the next threshold's percentage. - // let funds_surpass_threshold = - // till_threshold.checked_div_floor(recipient_percent).unwrap(); - - recip_coin.amount = coin.amount * recipient_percent; + // Make sure there's a next threshold in the first place + if threshold_index + 1 != conditional_splitter.thresholds.len() { + let next_threshold = &conditional_splitter.thresholds[threshold_index + 1].min; + let next_threshold_recipient_percent = + recipient_addr.percentages[threshold_index + 1]; + + // Check the amount remaining for the next threshold + let threshold_difference = next_threshold.checked_sub(funds_distributed)?; + + // If the funds received surpass the above amount, we will have to deal with crossing the threshold + if recip_coin.amount > threshold_difference { + // In this case the amount sent covers the difference between the upcoming threshold and the funds distributed, so we multiply that number by the current threshold's percentage + let first_threshold_amount = threshold_difference * recipient_percent; + + // The amount remaining after crossing the first threshold will be multiplied by the newely-crossed threshold's percentage + let second_threshold_amount = (recip_coin.amount - threshold_difference) + * next_threshold_recipient_percent; + + // The total amount to send will be the sum of both amounts + recip_coin.amount = + first_threshold_amount.checked_add(second_threshold_amount)?; + } else { + recip_coin.amount = coin.amount * recipient_percent; + } + } else { + recip_coin.amount = coin.amount * recipient_percent; + } remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; vec_coin.push(recip_coin.clone()); @@ -197,11 +216,6 @@ fn execute_send(ctx: ExecuteContext) -> Result { remainder_funds.retain(|x| x.amount > Uint128::zero()); - // Why does the remaining funds go the the sender of the executor of the splitter? - // Is it considered tax(fee) or mistake? - // Discussion around caller of splitter function in andromedaSPLITTER smart contract. - // From tests, it looks like owner of smart contract (Andromeda) will recieve the rest of funds. - // If so, should be documented if !remainder_funds.is_empty() { msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 2f3448227..2a88c1827 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -192,6 +192,8 @@ fn test_execute_send() { Decimal::from_ratio(Uint128::one(), Uint128::new(2)), // 20% Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), ], ), AddressPercentages::new( @@ -201,12 +203,15 @@ fn test_execute_send() { Decimal::from_ratio(Uint128::one(), Uint128::new(5)), // 10% Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), ], ), ], thresholds: vec![ Threshold::new(Uint128::zero()), Threshold::new(second_threshold), + Threshold::new(Uint128::new(50)), ], lock_time: Some(Milliseconds::from_seconds(100_000)), }; @@ -220,7 +225,7 @@ fn test_execute_send() { // Second batch will be used to test the second threshold let second_batch = 10u128; - // Third batch will be used to test the second threshold + // Third batch will be used to test crossing a threshold let third_batch = 100u128; // First batch @@ -324,6 +329,43 @@ fn test_execute_send() { let expected_funds_distributed = Uint128::new(10 + 10 + 100); assert_eq!(funds_distributed, expected_funds_distributed); + // amount of 30 * 20% + 70 * 50% = 41 + let amp_msg_1 = recip1 + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(41, "uandr")])) + .unwrap(); + // amount of 30 * 10% + 70 * 50% = 38 + let amp_msg_2 = recip2 + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(38, "uandr")])) + .unwrap(); + let amp_pkt = AMPPkt::new( + MOCK_CONTRACT_ADDR.to_string(), + MOCK_CONTRACT_ADDR.to_string(), + vec![amp_msg_1, amp_msg_2], + ); + let amp_msg = amp_pkt + .to_sub_msg( + MOCK_KERNEL_CONTRACT, + Some(vec![Coin::new(41, "uandr"), Coin::new(38, "uandr")]), + 1, + ) + .unwrap(); + + let expected_res = Response::new() + .add_submessages(vec![ + SubMsg::new( + // refunds remainder to sender + CosmosMsg::Bank(BankMsg::Send { + to_address: OWNER.to_string(), + amount: vec![Coin::new(21, "uandr")], // 100 - 41 - 38 remainder + }), + ), + amp_msg, + ]) + .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) + .add_submessage(generate_economics_message(OWNER, "Send")); + + assert_eq!(res, expected_res); + // let expected_res = Response::new() // .add_submessages(vec![ // SubMsg::new( From 088022a57c64a55cbef86ba1cc342a3fb381083a Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Fri, 26 Apr 2024 16:09:49 +0300 Subject: [PATCH 08/28] refactor: add address_percent field to Threshold --- .../src/contract.rs | 82 +++++-- .../src/testing/tests.rs | 220 ++++++++++-------- .../src/conditional_splitter.rs | 106 ++++----- 3 files changed, 231 insertions(+), 177 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index e68fdb4f1..0e272dca4 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -13,8 +13,8 @@ use andromeda_std::{ }; use andromeda_std::{ado_contract::ADOContract, common::context::ExecuteContext}; use cosmwasm_std::{ - attr, ensure, entry_point, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Reply, Response, StdError, SubMsg, Uint128, + attr, ensure, entry_point, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, + MessageInfo, Reply, Response, StdError, SubMsg, Uint128, }; use cw_utils::nonpayable; @@ -36,11 +36,10 @@ pub fn instantiate( let current_time = Milliseconds::from_seconds(env.block.time.seconds()); let mut conditional_splitter = ConditionalSplitter { - recipients: msg.recipients, thresholds: msg.thresholds.clone(), lock: msg.lock_time.unwrap_or_default(), }; - // Validate recipient list and thresholds + // Validate thresholds conditional_splitter.validate(deps.as_ref())?; match msg.lock_time { @@ -163,24 +162,29 @@ fn execute_send(ctx: ExecuteContext) -> Result { let funds_distributed = FUNDS_DISTRIBUTED.load(deps.storage)?; - for recipient_addr in &conditional_splitter.recipients { - // Get current range - let (_threshold, threshold_index) = - find_threshold(&conditional_splitter.thresholds, funds_distributed)?; - let recipient_percent = recipient_addr.percentages[threshold_index]; - + // Find the relevant threshold + let (threshold, threshold_index) = + find_threshold(&conditional_splitter.thresholds, funds_distributed)?; + for address_percent in threshold.address_percent { + let recipient_percent = address_percent.percent; let mut vec_coin: Vec = Vec::new(); for (i, coin) in info.funds.clone().iter().enumerate() { let mut recip_coin: Coin = coin.clone(); // Make sure there's a next threshold in the first place if threshold_index + 1 != conditional_splitter.thresholds.len() { - let next_threshold = &conditional_splitter.thresholds[threshold_index + 1].min; - let next_threshold_recipient_percent = - recipient_addr.percentages[threshold_index + 1]; + let next_threshold = &conditional_splitter.thresholds[threshold_index + 1]; + + // Find the recipient's percentage in the next threshold + let all_recipients = &next_threshold.address_percent; + + let next_threshold_recipient_percent: Decimal = all_recipients + .iter() + .find(|recipient| recipient.recipient == address_percent.recipient) + .map_or(Decimal::zero(), |recipient| recipient.percent); // Check the amount remaining for the next threshold - let threshold_difference = next_threshold.checked_sub(funds_distributed)?; + let threshold_difference = next_threshold.min.checked_sub(funds_distributed)?; // If the funds received surpass the above amount, we will have to deal with crossing the threshold if recip_coin.amount > threshold_difference { @@ -206,11 +210,59 @@ fn execute_send(ctx: ExecuteContext) -> Result { amp_funds.push(recip_coin); } - let amp_msg = recipient_addr + let amp_msg = address_percent .recipient .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; pkt = pkt.add_message(amp_msg); } + + // for recipient_addr in &conditional_splitter.recipients { + // // Get current range + // let (threshold, threshold_index) = + // find_threshold(&conditional_splitter.thresholds, funds_distributed)?; + + // let mut vec_coin: Vec = Vec::new(); + // for (i, coin) in info.funds.clone().iter().enumerate() { + // let mut recip_coin: Coin = coin.clone(); + + // // Make sure there's a next threshold in the first place + // if threshold_index + 1 != conditional_splitter.thresholds.len() { + // let next_threshold = &conditional_splitter.thresholds[threshold_index + 1].min; + // let next_threshold_recipient_percent = + // recipient_addr.percentages[threshold_index + 1]; + + // // Check the amount remaining for the next threshold + // let threshold_difference = next_threshold.checked_sub(funds_distributed)?; + + // // If the funds received surpass the above amount, we will have to deal with crossing the threshold + // if recip_coin.amount > threshold_difference { + // // In this case the amount sent covers the difference between the upcoming threshold and the funds distributed, so we multiply that number by the current threshold's percentage + // let first_threshold_amount = threshold_difference * recipient_percent; + + // // The amount remaining after crossing the first threshold will be multiplied by the newely-crossed threshold's percentage + // let second_threshold_amount = (recip_coin.amount - threshold_difference) + // * next_threshold_recipient_percent; + + // // The total amount to send will be the sum of both amounts + // recip_coin.amount = + // first_threshold_amount.checked_add(second_threshold_amount)?; + // } else { + // recip_coin.amount = coin.amount * recipient_percent; + // } + // } else { + // recip_coin.amount = coin.amount * recipient_percent; + // } + + // remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; + // vec_coin.push(recip_coin.clone()); + // amp_funds.push(recip_coin); + // } + + // let amp_msg = recipient_addr + // .recipient + // .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; + // pkt = pkt.add_message(amp_msg); + // } let new_funds_distributed = info.funds.first().unwrap().amount + funds_distributed; FUNDS_DISTRIBUTED.save(deps.storage, &new_funds_distributed)?; diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 2a88c1827..794e98dbc 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -21,25 +21,33 @@ use crate::{ state::{CONDITIONAL_SPLITTER, FUNDS_DISTRIBUTED}, testing::mock_querier::mock_dependencies_custom, }; -use andromeda_finance::conditional_splitter::{ - AddressPercentages, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, - InstantiateMsg, QueryMsg, Threshold, +use andromeda_finance::{ + conditional_splitter::{ + AddressPercentages, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, + InstantiateMsg, QueryMsg, Threshold, + }, + splitter::AddressPercent, }; fn init(deps: DepsMut) -> Response { let msg = InstantiateMsg { owner: Some(OWNER.to_owned()), kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - recipients: vec![AddressPercentages::new( - Recipient::from_string(String::from("some_address")), - vec![ - Decimal::from_ratio(Uint128::one(), Uint128::new(2)), - Decimal::from_ratio(Uint128::one(), Uint128::new(2)), - ], - )], thresholds: vec![ - Threshold::new(Uint128::zero()), - Threshold::new(Uint128::new(11)), + Threshold::new( + Uint128::zero(), + vec![AddressPercent::new( + Recipient::from_string(String::from("some_address")), + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + )], + ), + Threshold::new( + Uint128::new(11), + vec![AddressPercent::new( + Recipient::from_string(String::from("some_address")), + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + )], + ), ], lock_time: Some(Milliseconds::from_seconds(100_000)), }; @@ -67,10 +75,10 @@ fn test_execute_update_lock() { // Start off with an expiration that's behind current time (expired) let splitter = ConditionalSplitter { - recipients: vec![], lock: Milliseconds::from_seconds(current_time - 1), thresholds: vec![Threshold { min: Uint128::zero(), + address_percent: vec![], }], }; @@ -184,34 +192,46 @@ fn test_execute_send() { let msg = InstantiateMsg { owner: Some(OWNER.to_owned()), kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - recipients: vec![ - AddressPercentages::new( - recip1.clone(), + thresholds: vec![ + Threshold::new( + Uint128::zero(), vec![ - // 50% - Decimal::from_ratio(Uint128::one(), Uint128::new(2)), - // 20% - Decimal::from_ratio(Uint128::one(), Uint128::new(5)), - // 50% - Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + AddressPercent::new( + recip1.clone(), // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + AddressPercent::new( + recip2.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), ], ), - AddressPercentages::new( - recip2.clone(), + Threshold::new( + second_threshold, vec![ - // 20% - Decimal::from_ratio(Uint128::one(), Uint128::new(5)), - // 10% - Decimal::from_ratio(Uint128::one(), Uint128::new(10)), - // 50% - Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + AddressPercent::new( + recip1.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + AddressPercent::new( + recip2.clone(), // 10% + Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + ), + ], + ), + Threshold::new( + Uint128::new(50), + vec![ + AddressPercent::new( + recip1.clone(), // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + AddressPercent::new( + recip2.clone(), // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), ], ), - ], - thresholds: vec![ - Threshold::new(Uint128::zero()), - Threshold::new(second_threshold), - Threshold::new(Uint128::new(50)), ], lock_time: Some(Milliseconds::from_seconds(100_000)), }; @@ -461,81 +481,81 @@ fn test_execute_send() { // assert_eq!(res, expected_res); // } -#[test] -fn test_handle_packet_exit_with_error_true() { - let mut deps = mock_dependencies_custom(&[]); - let env = mock_env(); - let _res: Response = init(deps.as_mut()); +// #[test] +// fn test_handle_packet_exit_with_error_true() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let _res: Response = init(deps.as_mut()); - let sender_funds_amount = 0u128; - let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); +// let sender_funds_amount = 0u128; +// let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); - let recip_address1 = "address1".to_string(); - let recip_percent1 = 10; // 10% - - let recip_percent2 = 20; // 20% - - let recipient = vec![ - AddressPercentages { - recipient: Recipient::from_string(recip_address1.clone()), - percentages: vec![Decimal::percent(recip_percent1)], - }, - AddressPercentages { - recipient: Recipient::from_string(recip_address1.clone()), - percentages: vec![Decimal::percent(recip_percent2)], - }, - ]; - let pkt = AMPPkt::new( - info.clone().sender, - "cosmos2contract", - vec![AMPMsg::new( - recip_address1, - to_json_binary(&ExecuteMsg::Send {}).unwrap(), - Some(vec![Coin::new(0, "uluna")]), - )], - ); - let msg = ExecuteMsg::AMPReceive(pkt); +// let recip_address1 = "address1".to_string(); +// let recip_percent1 = 10; // 10% - let splitter = ConditionalSplitter { - recipients: recipient, - lock: Milliseconds::default(), - thresholds: vec![Threshold::new(Uint128::zero())], - }; +// let recip_percent2 = 20; // 20% - CONDITIONAL_SPLITTER - .save(deps.as_mut().storage, &splitter) - .unwrap(); +// let recipient = vec![ +// AddressPercentages { +// recipient: Recipient::from_string(recip_address1.clone()), +// percentages: vec![Decimal::percent(recip_percent1)], +// }, +// AddressPercentages { +// recipient: Recipient::from_string(recip_address1.clone()), +// percentages: vec![Decimal::percent(recip_percent2)], +// }, +// ]; +// let pkt = AMPPkt::new( +// info.clone().sender, +// "cosmos2contract", +// vec![AMPMsg::new( +// recip_address1, +// to_json_binary(&ExecuteMsg::Send {}).unwrap(), +// Some(vec![Coin::new(0, "uluna")]), +// )], +// ); +// let msg = ExecuteMsg::AMPReceive(pkt); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); +// let splitter = ConditionalSplitter { +// recipients: recipient, +// lock: Milliseconds::default(), +// thresholds: vec![Threshold::new(Uint128::zero())], +// }; - assert_eq!( - err, - ContractError::InvalidFunds { - msg: "Amount must be non-zero".to_string(), - } - ); -} +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); -#[test] -fn test_query_splitter() { - let mut deps = mock_dependencies_custom(&[]); - let env = mock_env(); - let splitter = ConditionalSplitter { - recipients: vec![], - lock: Milliseconds::default(), - thresholds: vec![Threshold::new(Uint128::zero())], - }; +// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - CONDITIONAL_SPLITTER - .save(deps.as_mut().storage, &splitter) - .unwrap(); +// assert_eq!( +// err, +// ContractError::InvalidFunds { +// msg: "Amount must be non-zero".to_string(), +// } +// ); +// } - let query_msg = QueryMsg::GetConditionalSplitterConfig {}; - let res = query(deps.as_ref(), env, query_msg).unwrap(); - let val: GetConditionalSplitterConfigResponse = from_json(res).unwrap(); +// #[test] +// fn test_query_splitter() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// let splitter = ConditionalSplitter { +// recipients: vec![], +// lock: Milliseconds::default(), +// thresholds: vec![Threshold::new(Uint128::zero())], +// }; - assert_eq!(val.config, splitter); -} +// CONDITIONAL_SPLITTER +// .save(deps.as_mut().storage, &splitter) +// .unwrap(); + +// let query_msg = QueryMsg::GetConditionalSplitterConfig {}; +// let res = query(deps.as_ref(), env, query_msg).unwrap(); +// let val: GetConditionalSplitterConfigResponse = from_json(res).unwrap(); + +// assert_eq!(val.config, splitter); +// } // #[test] // fn test_execute_send_error() { diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 146a7a896..7efb16b5f 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -8,14 +8,20 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ensure, Decimal, Deps, Uint128}; use std::collections::HashSet; +use crate::splitter::AddressPercent; + // The contract owner will input a vector of Threshold #[cw_serde] pub struct Threshold { pub min: Uint128, + pub address_percent: Vec, } impl Threshold { - pub fn new(min: Uint128) -> Self { - Self { min } + pub fn new(min: Uint128, address_percent: Vec) -> Self { + Self { + min, + address_percent, + } } pub fn in_range(&self, num: Uint128) -> bool { num >= self.min @@ -62,8 +68,6 @@ impl AddressPercentages { #[cw_serde] /// A config struct for a `Conditional Splitter` contract. pub struct ConditionalSplitter { - /// The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on the threshold. - pub recipients: Vec, /// The vector of thresholds which assign a percentage for a certain range of received funds pub thresholds: Vec, /// The lock's expiration time @@ -71,8 +75,7 @@ pub struct ConditionalSplitter { } impl ConditionalSplitter { pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { - validate_recipient_list(deps, self.recipients.clone())?; - validate_thresholds(&self.thresholds, &self.recipients) + validate_thresholds(deps, &self.thresholds) } } @@ -81,7 +84,6 @@ impl ConditionalSplitter { pub struct InstantiateMsg { /// The vector of recipients for the contract. Anytime a `Send` execute message is /// sent the amount sent will be divided amongst these recipients depending on their assigned percentage. - pub recipients: Vec, pub thresholds: Vec, pub lock_time: Option, } @@ -114,35 +116,37 @@ pub struct GetConditionalSplitterConfigResponse { pub config: ConditionalSplitter, } -/// Ensures that a given list of recipients for a `splitter` contract is valid: -/// * Percentages of corresponding indexes should not sum up to over 100 -/// * Must include at least one recipient -/// * The number of recipients must not exceed 100 -/// * The recipient addresses must be unique -pub fn validate_recipient_list( - deps: Deps, - recipients: Vec, -) -> Result<(), ContractError> { - ensure!( - !recipients.is_empty(), - ContractError::EmptyRecipientsList {} - ); - ensure!( - recipients.len() <= 100, - ContractError::ReachedRecipientLimit {} - ); - - for i in 0..recipients[0].percentages.len() { - // Collect the ith percentage of each recipient - let mut i_percentages = Decimal::zero(); +/// Ensures that a given list of recipients for a `conditional splitter` contract is valid: +/// * Percentages of each threshold should not exceed 100 +/// * Each threshold must include at least one recipient +/// * The number of recipients for each threshold must not exceed 100 +/// * The recipient addresses must be unique for each threshold +/// * Make sure there are no duplicate min values between the thresholds + +pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<(), ContractError> { + let mut min_value_set = HashSet::new(); + + for threshold in thresholds { + // Make sure the threshold has recipients + ensure!( + !threshold.address_percent.is_empty(), + ContractError::EmptyRecipientsList {} + ); + // Make sure the threshold's number of recipients doesn't exceed 100 + ensure!( + threshold.address_percent.len() <= 100, + ContractError::ReachedRecipientLimit {} + ); + + let mut total_percent = Decimal::zero(); let mut recipient_address_set = HashSet::new(); - for recipient in &recipients { - // Check for invalid percentages - i_percentages = i_percentages.checked_add(recipient.percentages[i])?; + + for address_percent in &threshold.address_percent { + total_percent = total_percent.checked_add(address_percent.percent)?; // Checks for duplicate and invalid recipients - recipient.recipient.validate(&deps)?; - let recipient_address = recipient.recipient.address.get_raw_address(&deps)?; + address_percent.recipient.validate(&deps)?; + let recipient_address = address_percent.recipient.address.get_raw_address(&deps)?; ensure!( !recipient_address_set.contains(&recipient_address), ContractError::DuplicateRecipient {} @@ -150,41 +154,19 @@ pub fn validate_recipient_list( recipient_address_set.insert(recipient_address); } ensure!( - i_percentages <= Decimal::one(), + total_percent <= Decimal::one(), ContractError::AmountExceededHundredPrecent {} ); - } - Ok(()) -} -/// Makes sure the percentages don't exceed 100 and that there are no duplicate min values -pub fn validate_thresholds( - thresholds: &Vec, - recipients: &Vec, -) -> Result<(), ContractError> { - let number_of_thresholds = thresholds.len(); - - // Check that each recipient has the same amount of percentages as thresholds - for recipient in recipients { + // Checks for duplicate minimum values + let min_value = threshold.min.u128(); ensure!( - recipient.percentages.len() == number_of_thresholds, - ContractError::ThresholdsPercentagesDiscrepancy { - msg: format!( - "The number of thresholds is: {:?}, whereas the numer of percentages is: {:?}", - number_of_thresholds, - recipient.percentages.len() - ) - } + !min_value_set.contains(&min_value), + ContractError::DuplicateRecipient {} ); - } - - // Check that there are no duplicate minimum values - let min_values: HashSet<_> = thresholds.iter().map(|t| t.min.u128()).collect(); - ensure!( - min_values.len() == thresholds.len(), - ContractError::DuplicateThresholds {} - ); + min_value_set.insert(min_value); + } Ok(()) } From 6920b4af695ce0146cc4d51dfde531d1203c69e1 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Fri, 26 Apr 2024 16:13:05 +0300 Subject: [PATCH 09/28] chore: removed unused code --- .../src/contract.rs | 47 ---------------- .../src/testing/tests.rs | 53 +++++++------------ .../src/conditional_splitter.rs | 17 ------ 3 files changed, 18 insertions(+), 99 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 0e272dca4..e132181e6 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -216,53 +216,6 @@ fn execute_send(ctx: ExecuteContext) -> Result { pkt = pkt.add_message(amp_msg); } - // for recipient_addr in &conditional_splitter.recipients { - // // Get current range - // let (threshold, threshold_index) = - // find_threshold(&conditional_splitter.thresholds, funds_distributed)?; - - // let mut vec_coin: Vec = Vec::new(); - // for (i, coin) in info.funds.clone().iter().enumerate() { - // let mut recip_coin: Coin = coin.clone(); - - // // Make sure there's a next threshold in the first place - // if threshold_index + 1 != conditional_splitter.thresholds.len() { - // let next_threshold = &conditional_splitter.thresholds[threshold_index + 1].min; - // let next_threshold_recipient_percent = - // recipient_addr.percentages[threshold_index + 1]; - - // // Check the amount remaining for the next threshold - // let threshold_difference = next_threshold.checked_sub(funds_distributed)?; - - // // If the funds received surpass the above amount, we will have to deal with crossing the threshold - // if recip_coin.amount > threshold_difference { - // // In this case the amount sent covers the difference between the upcoming threshold and the funds distributed, so we multiply that number by the current threshold's percentage - // let first_threshold_amount = threshold_difference * recipient_percent; - - // // The amount remaining after crossing the first threshold will be multiplied by the newely-crossed threshold's percentage - // let second_threshold_amount = (recip_coin.amount - threshold_difference) - // * next_threshold_recipient_percent; - - // // The total amount to send will be the sum of both amounts - // recip_coin.amount = - // first_threshold_amount.checked_add(second_threshold_amount)?; - // } else { - // recip_coin.amount = coin.amount * recipient_percent; - // } - // } else { - // recip_coin.amount = coin.amount * recipient_percent; - // } - - // remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; - // vec_coin.push(recip_coin.clone()); - // amp_funds.push(recip_coin); - // } - - // let amp_msg = recipient_addr - // .recipient - // .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; - // pkt = pkt.add_message(amp_msg); - // } let new_funds_distributed = info.funds.first().unwrap().amount + funds_distributed; FUNDS_DISTRIBUTED.save(deps.storage, &new_funds_distributed)?; diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 794e98dbc..ef3ee011d 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -23,8 +23,8 @@ use crate::{ }; use andromeda_finance::{ conditional_splitter::{ - AddressPercentages, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, - InstantiateMsg, QueryMsg, Threshold, + ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, InstantiateMsg, + QueryMsg, Threshold, }, splitter::AddressPercent, }; @@ -385,22 +385,6 @@ fn test_execute_send() { .add_submessage(generate_economics_message(OWNER, "Send")); assert_eq!(res, expected_res); - - // let expected_res = Response::new() - // .add_submessages(vec![ - // SubMsg::new( - // // refunds remainder to sender - // CosmosMsg::Bank(BankMsg::Send { - // to_address: OWNER.to_string(), - // amount: vec![Coin::new(7000, "uluna")], // 10000 * 0.7 remainder - // }), - // ), - // amp_msg, - // ]) - // .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) - // .add_submessage(generate_economics_message(OWNER, "Send")); - - // assert_eq!(res, expected_res); } // #[test] @@ -536,26 +520,25 @@ fn test_execute_send() { // ); // } -// #[test] -// fn test_query_splitter() { -// let mut deps = mock_dependencies_custom(&[]); -// let env = mock_env(); -// let splitter = ConditionalSplitter { -// recipients: vec![], -// lock: Milliseconds::default(), -// thresholds: vec![Threshold::new(Uint128::zero())], -// }; +#[test] +fn test_query_splitter() { + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); + let splitter = ConditionalSplitter { + lock: Milliseconds::default(), + thresholds: vec![Threshold::new(Uint128::zero(), vec![])], + }; -// CONDITIONAL_SPLITTER -// .save(deps.as_mut().storage, &splitter) -// .unwrap(); + CONDITIONAL_SPLITTER + .save(deps.as_mut().storage, &splitter) + .unwrap(); -// let query_msg = QueryMsg::GetConditionalSplitterConfig {}; -// let res = query(deps.as_ref(), env, query_msg).unwrap(); -// let val: GetConditionalSplitterConfigResponse = from_json(res).unwrap(); + let query_msg = QueryMsg::GetConditionalSplitterConfig {}; + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let val: GetConditionalSplitterConfigResponse = from_json(res).unwrap(); -// assert_eq!(val.config, splitter); -// } + assert_eq!(val.config, splitter); +} // #[test] // fn test_execute_send_error() { diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 7efb16b5f..be86a4e2f 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -48,23 +48,6 @@ pub fn find_threshold( Err(ContractError::InvalidRange {}) } -#[cw_serde] -pub struct AddressPercentages { - pub recipient: Recipient, - // The sequence of the the percentages should correspond to each threshold. - // For example the first value in percentages should correspond to the first threshold - pub percentages: Vec, -} - -impl AddressPercentages { - pub fn new(recipient: Recipient, percentages: Vec) -> Self { - Self { - recipient, - percentages, - } - } -} - #[cw_serde] /// A config struct for a `Conditional Splitter` contract. pub struct ConditionalSplitter { From d1adcb434f6168b6cb938c3a02b1263e453d1a7f Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Fri, 26 Apr 2024 17:15:51 +0300 Subject: [PATCH 10/28] test: test_validate_thresholds --- .../src/testing/tests.rs | 194 +++++++++--------- .../src/conditional_splitter.rs | 188 +++++++++++------ 2 files changed, 223 insertions(+), 159 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index ef3ee011d..3c8b3cd31 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -465,60 +465,59 @@ fn test_execute_send() { // assert_eq!(res, expected_res); // } -// #[test] -// fn test_handle_packet_exit_with_error_true() { -// let mut deps = mock_dependencies_custom(&[]); -// let env = mock_env(); -// let _res: Response = init(deps.as_mut()); - -// let sender_funds_amount = 0u128; -// let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); - -// let recip_address1 = "address1".to_string(); -// let recip_percent1 = 10; // 10% +#[test] +fn test_handle_packet_exit_with_error_true() { + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); + let _res: Response = init(deps.as_mut()); -// let recip_percent2 = 20; // 20% + let sender_funds_amount = 0u128; + let info = mock_info(OWNER, &[Coin::new(sender_funds_amount, "uluna")]); -// let recipient = vec![ -// AddressPercentages { -// recipient: Recipient::from_string(recip_address1.clone()), -// percentages: vec![Decimal::percent(recip_percent1)], -// }, -// AddressPercentages { -// recipient: Recipient::from_string(recip_address1.clone()), -// percentages: vec![Decimal::percent(recip_percent2)], -// }, -// ]; -// let pkt = AMPPkt::new( -// info.clone().sender, -// "cosmos2contract", -// vec![AMPMsg::new( -// recip_address1, -// to_json_binary(&ExecuteMsg::Send {}).unwrap(), -// Some(vec![Coin::new(0, "uluna")]), -// )], -// ); -// let msg = ExecuteMsg::AMPReceive(pkt); + let recip_address1 = "address1".to_string(); + let recip_percent1 = 10; // 10% + + let recip_percent2 = 20; // 20% + + let address_percent = vec![ + AddressPercent { + recipient: Recipient::from_string(recip_address1.clone()), + percent: Decimal::percent(recip_percent1), + }, + AddressPercent { + recipient: Recipient::from_string(recip_address1.clone()), + percent: Decimal::percent(recip_percent2), + }, + ]; + let pkt = AMPPkt::new( + info.clone().sender, + "cosmos2contract", + vec![AMPMsg::new( + recip_address1, + to_json_binary(&ExecuteMsg::Send {}).unwrap(), + Some(vec![Coin::new(0, "uluna")]), + )], + ); + let msg = ExecuteMsg::AMPReceive(pkt); -// let splitter = ConditionalSplitter { -// recipients: recipient, -// lock: Milliseconds::default(), -// thresholds: vec![Threshold::new(Uint128::zero())], -// }; + let splitter = ConditionalSplitter { + lock: Milliseconds::default(), + thresholds: vec![Threshold::new(Uint128::zero(), address_percent)], + }; -// CONDITIONAL_SPLITTER -// .save(deps.as_mut().storage, &splitter) -// .unwrap(); + CONDITIONAL_SPLITTER + .save(deps.as_mut().storage, &splitter) + .unwrap(); -// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); -// assert_eq!( -// err, -// ContractError::InvalidFunds { -// msg: "Amount must be non-zero".to_string(), -// } -// ); -// } + assert_eq!( + err, + ContractError::InvalidFunds { + msg: "Amount must be non-zero".to_string(), + } + ); +} #[test] fn test_query_splitter() { @@ -540,60 +539,60 @@ fn test_query_splitter() { assert_eq!(val.config, splitter); } -// #[test] -// fn test_execute_send_error() { -// //Executes send with more than 5 tokens [ACK-04] -// let mut deps = mock_dependencies_custom(&[]); -// let env = mock_env(); -// let _res: Response = init(deps.as_mut()); - -// let sender_funds_amount = 10000u128; -// let owner = "creator"; -// let info = mock_info( -// owner, -// &vec![ -// Coin::new(sender_funds_amount, "uluna"), -// Coin::new(sender_funds_amount, "uluna"), -// Coin::new(sender_funds_amount, "uluna"), -// Coin::new(sender_funds_amount, "uluna"), -// Coin::new(sender_funds_amount, "uluna"), -// Coin::new(sender_funds_amount, "uluna"), -// ], -// ); +#[test] +fn test_execute_send_error() { + //Executes send with more than 5 tokens [ACK-04] + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); + let _res: Response = init(deps.as_mut()); -// let recip_address1 = "address1".to_string(); -// let recip_percent1 = 10; // 10% + let sender_funds_amount = 10000u128; + let owner = "creator"; + let info = mock_info( + owner, + &vec![ + Coin::new(sender_funds_amount, "uluna"), + Coin::new(sender_funds_amount, "uluna"), + Coin::new(sender_funds_amount, "uluna"), + Coin::new(sender_funds_amount, "uluna"), + Coin::new(sender_funds_amount, "uluna"), + Coin::new(sender_funds_amount, "uluna"), + ], + ); -// let recip_address2 = "address2".to_string(); -// let recip_percent2 = 20; // 20% + let recip_address1 = "address1".to_string(); + let recip_percent1 = 10; // 10% -// let recipient = vec![ -// AddressFunds { -// recipient: Recipient::from_string(recip_address1), -// percent: Decimal::percent(recip_percent1), -// }, -// AddressFunds { -// recipient: Recipient::from_string(recip_address2), -// percent: Decimal::percent(recip_percent2), -// }, -// ]; -// let msg = ExecuteMsg::Send {}; + let recip_address2 = "address2".to_string(); + let recip_percent2 = 20; // 20% + + let address_percent = vec![ + AddressPercent { + recipient: Recipient::from_string(recip_address1), + percent: Decimal::percent(recip_percent1), + }, + AddressPercent { + recipient: Recipient::from_string(recip_address2), + percent: Decimal::percent(recip_percent2), + }, + ]; + let msg = ExecuteMsg::Send {}; -// let splitter = ConditionalSplitter { -// recipients: recipient, -// lock: Milliseconds::default(), -// }; + let splitter = ConditionalSplitter { + thresholds: vec![Threshold::new(Uint128::zero(), address_percent)], + lock: Milliseconds::default(), + }; -// CONDITIONAL_SPLITTER -// .save(deps.as_mut().storage, &splitter) -// .unwrap(); + CONDITIONAL_SPLITTER + .save(deps.as_mut().storage, &splitter) + .unwrap(); -// let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); -// let expected_res = ContractError::ExceedsMaxAllowedCoins {}; + let expected_res = ContractError::ExceedsMaxAllowedCoins {}; -// assert_eq!(res, expected_res); -// } + assert_eq!(res, expected_res); +} #[test] fn test_update_app_contract() { @@ -629,12 +628,5 @@ fn test_update_app_contract_invalid_recipient() { }; let res = execute(deps.as_mut(), mock_env(), info, msg); - - // assert_eq!( - // ContractError::InvalidComponent { - // name: "z".to_string() - // }, - // res.unwrap_err() - // ); assert!(res.is_err()) } diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index be86a4e2f..0eeb65b1b 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -145,7 +145,7 @@ pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<() let min_value = threshold.min.u128(); ensure!( !min_value_set.contains(&min_value), - ContractError::DuplicateRecipient {} + ContractError::DuplicateThresholds {} ); min_value_set.insert(min_value); @@ -153,60 +153,132 @@ pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<() Ok(()) } -// #[cfg(test)] -// mod tests { -// use cosmwasm_std::testing::mock_dependencies; - -// use super::*; - -// #[test] -// fn test_validate_recipient_list() { -// let deps = mock_dependencies(); -// let empty_recipients = vec![]; -// let res = validate_recipient_list(deps.as_ref(), empty_recipients).unwrap_err(); -// assert_eq!(res, ContractError::EmptyRecipientsList {}); - -// let inadequate_recipients = vec![AddressPercent { -// recipient: Recipient::from_string(String::from("abc")), -// percent: Decimal::percent(150), -// }]; -// let res = validate_recipient_list(deps.as_ref(), inadequate_recipients).unwrap_err(); -// assert_eq!(res, ContractError::AmountExceededHundredPrecent {}); - -// let duplicate_recipients = vec![ -// AddressPercent { -// recipient: Recipient::from_string(String::from("abc")), -// percent: Decimal::percent(50), -// }, -// AddressPercent { -// recipient: Recipient::from_string(String::from("abc")), -// percent: Decimal::percent(50), -// }, -// ]; - -// let err = validate_recipient_list(deps.as_ref(), duplicate_recipients).unwrap_err(); -// assert_eq!(err, ContractError::DuplicateRecipient {}); - -// let valid_recipients = vec![ -// AddressPercent { -// recipient: Recipient::from_string(String::from("abc")), -// percent: Decimal::percent(50), -// }, -// AddressPercent { -// recipient: Recipient::from_string(String::from("xyz")), -// percent: Decimal::percent(50), -// }, -// ]; - -// let res = validate_recipient_list(deps.as_ref(), valid_recipients); -// assert!(res.is_ok()); - -// let one_valid_recipient = vec![AddressPercent { -// recipient: Recipient::from_string(String::from("abc")), -// percent: Decimal::percent(50), -// }]; - -// let res = validate_recipient_list(deps.as_ref(), one_valid_recipient); -// assert!(res.is_ok()); -// } -// } +#[cfg(test)] +mod tests { + + use andromeda_std::amp::AndrAddr; + use cosmwasm_std::testing::mock_dependencies; + + use super::*; + + struct TestThresholdValidation { + name: &'static str, + thresholds: Vec, + expected_error: Option, + } + + #[test] + fn test_validate_thresholds() { + let test_cases = vec![ + TestThresholdValidation { + name: "Duplicate minimums between thresholds", + thresholds: vec![ + Threshold::new( + Uint128::zero(), + vec![AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + )], + ), + Threshold::new( + Uint128::zero(), + vec![AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + )], + ), + ], + expected_error: Some(ContractError::DuplicateThresholds {}), + }, + TestThresholdValidation { + name: "Duplicate recipients within the same threshold", + thresholds: vec![Threshold::new( + Uint128::zero(), + vec![ + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + ), + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + ), + ], + )], + expected_error: Some(ContractError::DuplicateRecipient {}), + }, + TestThresholdValidation { + name: "Sum of the threshold's percentage should not exceed 100", + thresholds: vec![Threshold::new( + Uint128::zero(), + vec![ + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::one(), + ), + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient2"), None), + Decimal::one(), + ), + ], + )], + expected_error: Some(ContractError::AmountExceededHundredPrecent {}), + }, + TestThresholdValidation { + name: "Threshold with no recipients", + thresholds: vec![Threshold::new(Uint128::zero(), vec![])], + expected_error: Some(ContractError::EmptyRecipientsList {}), + }, + TestThresholdValidation { + name: "Works with one threshold", + thresholds: vec![Threshold::new( + Uint128::zero(), + vec![AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + )], + )], + expected_error: None, + }, + TestThresholdValidation { + name: "Works with two thresholds", + thresholds: vec![ + Threshold::new( + Uint128::zero(), + vec![ + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + ), + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient2"), None), + Decimal::new(Uint128::new(20)), + ), + ], + ), + Threshold::new( + Uint128::one(), + vec![AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + )], + ), + ], + expected_error: None, + }, + ]; + + for test in test_cases { + let deps = mock_dependencies(); + + let res = validate_thresholds(deps.as_ref(), &test.thresholds); + + if let Some(err) = test.expected_error { + assert_eq!(res.unwrap_err(), err, "{}", test.name); + continue; + } else { + assert!(res.is_ok()) + } + } + } +} From a79c79aefacd37550c876f808a08c1f67ff9a824 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 10:24:20 +0300 Subject: [PATCH 11/28] refactor: early exit for total percentage exceeding 100 --- packages/andromeda-finance/src/conditional_splitter.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 0eeb65b1b..5358c2126 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -125,7 +125,12 @@ pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<() let mut recipient_address_set = HashSet::new(); for address_percent in &threshold.address_percent { + // Check for total percent exceeding 100 total_percent = total_percent.checked_add(address_percent.percent)?; + ensure!( + total_percent <= Decimal::one(), + ContractError::AmountExceededHundredPrecent {} + ); // Checks for duplicate and invalid recipients address_percent.recipient.validate(&deps)?; @@ -136,10 +141,6 @@ pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<() ); recipient_address_set.insert(recipient_address); } - ensure!( - total_percent <= Decimal::one(), - ContractError::AmountExceededHundredPrecent {} - ); // Checks for duplicate minimum values let min_value = threshold.min.u128(); From f9c996d03b6f951fe3e7133394f4b0181b2cfd1d Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 10:40:23 +0300 Subject: [PATCH 12/28] refactor: get next_threshold_recipient_percent after confirming that the threshold will be crossed, rename find_threshold to get_threshold --- .../src/contract.rs | 20 +++++++++---------- .../src/conditional_splitter.rs | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index e132181e6..1024c21f3 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -1,6 +1,6 @@ use crate::state::{CONDITIONAL_SPLITTER, FUNDS_DISTRIBUTED}; use andromeda_finance::conditional_splitter::{ - find_threshold, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, + get_threshold, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, InstantiateMsg, QueryMsg, }; use std::vec; @@ -164,7 +164,7 @@ fn execute_send(ctx: ExecuteContext) -> Result { // Find the relevant threshold let (threshold, threshold_index) = - find_threshold(&conditional_splitter.thresholds, funds_distributed)?; + get_threshold(&conditional_splitter.thresholds, funds_distributed)?; for address_percent in threshold.address_percent { let recipient_percent = address_percent.percent; let mut vec_coin: Vec = Vec::new(); @@ -175,19 +175,19 @@ fn execute_send(ctx: ExecuteContext) -> Result { if threshold_index + 1 != conditional_splitter.thresholds.len() { let next_threshold = &conditional_splitter.thresholds[threshold_index + 1]; - // Find the recipient's percentage in the next threshold - let all_recipients = &next_threshold.address_percent; - - let next_threshold_recipient_percent: Decimal = all_recipients - .iter() - .find(|recipient| recipient.recipient == address_percent.recipient) - .map_or(Decimal::zero(), |recipient| recipient.percent); - // Check the amount remaining for the next threshold let threshold_difference = next_threshold.min.checked_sub(funds_distributed)?; // If the funds received surpass the above amount, we will have to deal with crossing the threshold if recip_coin.amount > threshold_difference { + // Find the recipient's percentage in the next threshold + let all_recipients = &next_threshold.address_percent; + + let next_threshold_recipient_percent: Decimal = all_recipients + .iter() + .find(|recipient| recipient.recipient == address_percent.recipient) + .map_or(Decimal::zero(), |recipient| recipient.percent); + // In this case the amount sent covers the difference between the upcoming threshold and the funds distributed, so we multiply that number by the current threshold's percentage let first_threshold_amount = threshold_difference * recipient_percent; diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 5358c2126..5ce907d31 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -28,9 +28,9 @@ impl Threshold { } } -pub fn find_threshold( +pub fn get_threshold( thresholds: &[Threshold], - num: Uint128, + amount: Uint128, ) -> Result<(Threshold, usize), ContractError> { // Create a vector of tuples containing the original index and the threshold let mut indexed_thresholds: Vec<(usize, &Threshold)> = thresholds.iter().enumerate().collect(); @@ -40,7 +40,7 @@ pub fn find_threshold( // Iterate over the sorted indexed thresholds for (index, threshold) in indexed_thresholds { - if threshold.in_range(num) { + if threshold.in_range(amount) { // Return the threshold and its original index return Ok((threshold.clone(), index)); } From d51e6f14c3f6e9e68f27bef8b6fb0675638c957c Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 13:17:09 +0300 Subject: [PATCH 13/28] refactor: remove funds_distributed, threshold solely depends on the funds sent and there's no memory of previously sent funds --- .../src/contract.rs | 55 +++---------------- .../src/state.rs | 3 +- .../src/testing/tests.rs | 50 +++++------------ .../src/conditional_splitter.rs | 24 ++++---- 4 files changed, 35 insertions(+), 97 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 1024c21f3..fb34e2c05 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -1,4 +1,4 @@ -use crate::state::{CONDITIONAL_SPLITTER, FUNDS_DISTRIBUTED}; +use crate::state::CONDITIONAL_SPLITTER; use andromeda_finance::conditional_splitter::{ get_threshold, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, InstantiateMsg, QueryMsg, @@ -13,8 +13,8 @@ use andromeda_std::{ }; use andromeda_std::{ado_contract::ADOContract, common::context::ExecuteContext}; use cosmwasm_std::{ - attr, ensure, entry_point, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, - MessageInfo, Reply, Response, StdError, SubMsg, Uint128, + attr, ensure, entry_point, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Reply, Response, StdError, SubMsg, Uint128, }; use cw_utils::nonpayable; @@ -62,7 +62,6 @@ pub fn instantiate( } // Save kernel address after validating it CONDITIONAL_SPLITTER.save(deps.storage, &conditional_splitter)?; - FUNDS_DISTRIBUTED.save(deps.storage, &Uint128::zero())?; let inst_resp = ADOContract::default().instantiate( deps.storage, @@ -160,51 +159,18 @@ fn execute_send(ctx: ExecuteContext) -> Result { let mut pkt = AMPPkt::from_ctx(ctx.amp_ctx, ctx.env.contract.address.to_string()); - let funds_distributed = FUNDS_DISTRIBUTED.load(deps.storage)?; - // Find the relevant threshold - let (threshold, threshold_index) = - get_threshold(&conditional_splitter.thresholds, funds_distributed)?; + let threshold = get_threshold( + &conditional_splitter.thresholds, + remainder_funds.first().unwrap().amount, + )?; + for address_percent in threshold.address_percent { let recipient_percent = address_percent.percent; let mut vec_coin: Vec = Vec::new(); for (i, coin) in info.funds.clone().iter().enumerate() { let mut recip_coin: Coin = coin.clone(); - - // Make sure there's a next threshold in the first place - if threshold_index + 1 != conditional_splitter.thresholds.len() { - let next_threshold = &conditional_splitter.thresholds[threshold_index + 1]; - - // Check the amount remaining for the next threshold - let threshold_difference = next_threshold.min.checked_sub(funds_distributed)?; - - // If the funds received surpass the above amount, we will have to deal with crossing the threshold - if recip_coin.amount > threshold_difference { - // Find the recipient's percentage in the next threshold - let all_recipients = &next_threshold.address_percent; - - let next_threshold_recipient_percent: Decimal = all_recipients - .iter() - .find(|recipient| recipient.recipient == address_percent.recipient) - .map_or(Decimal::zero(), |recipient| recipient.percent); - - // In this case the amount sent covers the difference between the upcoming threshold and the funds distributed, so we multiply that number by the current threshold's percentage - let first_threshold_amount = threshold_difference * recipient_percent; - - // The amount remaining after crossing the first threshold will be multiplied by the newely-crossed threshold's percentage - let second_threshold_amount = (recip_coin.amount - threshold_difference) - * next_threshold_recipient_percent; - - // The total amount to send will be the sum of both amounts - recip_coin.amount = - first_threshold_amount.checked_add(second_threshold_amount)?; - } else { - recip_coin.amount = coin.amount * recipient_percent; - } - } else { - recip_coin.amount = coin.amount * recipient_percent; - } - + recip_coin.amount = coin.amount * recipient_percent; remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; vec_coin.push(recip_coin.clone()); amp_funds.push(recip_coin); @@ -216,9 +182,6 @@ fn execute_send(ctx: ExecuteContext) -> Result { pkt = pkt.add_message(amp_msg); } - let new_funds_distributed = info.funds.first().unwrap().amount + funds_distributed; - FUNDS_DISTRIBUTED.save(deps.storage, &new_funds_distributed)?; - remainder_funds.retain(|x| x.amount > Uint128::zero()); if !remainder_funds.is_empty() { diff --git a/contracts/finance/andromeda-conditional-splitter/src/state.rs b/contracts/finance/andromeda-conditional-splitter/src/state.rs index c5e95a7f0..4f7360606 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/state.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/state.rs @@ -1,7 +1,6 @@ use andromeda_finance::conditional_splitter::ConditionalSplitter; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Addr; use cw_storage_plus::Item; pub const CONDITIONAL_SPLITTER: Item = Item::new("conditional_splitter"); pub const KERNEL_ADDRESS: Item = Item::new("kernel_address"); -pub const FUNDS_DISTRIBUTED: Item = Item::new("funds_distributed"); diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 3c8b3cd31..742e43796 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -18,7 +18,7 @@ use super::mock_querier::MOCK_KERNEL_CONTRACT; use crate::{ contract::{execute, instantiate, query}, - state::{CONDITIONAL_SPLITTER, FUNDS_DISTRIBUTED}, + state::CONDITIONAL_SPLITTER, testing::mock_querier::mock_dependencies_custom, }; use andromeda_finance::{ @@ -239,8 +239,8 @@ fn test_execute_send() { let info = mock_info("owner", &[]); instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - // First batch of funds which will put funds received to 10 and push the next batch to the other threshold - let first_batch = 10u128; + // First batch will test first threshold + let first_batch = 8u128; // Second batch will be used to test the second threshold let second_batch = 10u128; @@ -255,11 +255,11 @@ fn test_execute_send() { // 50 percent let amp_msg_1 = recip1 - .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(5, "uandr")])) + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(4, "uandr")])) .unwrap(); - // 20 percent + // 20 percent, 1.6 which is rounded down to 1 let amp_msg_2 = recip2 - .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(2, "uandr")])) + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(1, "uandr")])) .unwrap(); let amp_pkt = AMPPkt::new( MOCK_CONTRACT_ADDR.to_string(), @@ -269,7 +269,7 @@ fn test_execute_send() { let amp_msg = amp_pkt .to_sub_msg( MOCK_KERNEL_CONTRACT, - Some(vec![Coin::new(5, "uandr"), Coin::new(2, "uandr")]), + Some(vec![Coin::new(4, "uandr"), Coin::new(1, "uandr")]), 1, ) .unwrap(); @@ -280,7 +280,7 @@ fn test_execute_send() { // refunds remainder to sender CosmosMsg::Bank(BankMsg::Send { to_address: OWNER.to_string(), - amount: vec![Coin::new(3, "uandr")], // 10 - (0.5 * 10) - (0.2 * 10) remainder + amount: vec![Coin::new(3, "uandr")], // 8 - (0.5 * 8) - (0.2 * 8) remainder }), ), amp_msg, @@ -290,19 +290,11 @@ fn test_execute_send() { assert_eq!(res, expected_res); - let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); - let expected_funds_distributed = Uint128::new(10); - assert_eq!(funds_distributed, expected_funds_distributed); - // Second batch let info = mock_info(OWNER, &[Coin::new(second_batch, "uandr")]); let msg = ExecuteMsg::Send {}; let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); - let expected_funds_distributed = Uint128::new(10 + 10); - assert_eq!(funds_distributed, expected_funds_distributed); - // 20 percent let amp_msg_1 = recip1 .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(2, "uandr")])) @@ -345,17 +337,13 @@ fn test_execute_send() { let msg = ExecuteMsg::Send {}; let res = execute(deps.as_mut(), env, info, msg).unwrap(); - let funds_distributed = FUNDS_DISTRIBUTED.load(&deps.storage).unwrap(); - let expected_funds_distributed = Uint128::new(10 + 10 + 100); - assert_eq!(funds_distributed, expected_funds_distributed); - - // amount of 30 * 20% + 70 * 50% = 41 + // amount 100 * 50% = 50 let amp_msg_1 = recip1 - .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(41, "uandr")])) + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(50, "uandr")])) .unwrap(); - // amount of 30 * 10% + 70 * 50% = 38 + // amount 100 * 50% = 50 let amp_msg_2 = recip2 - .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(38, "uandr")])) + .generate_amp_msg(&deps.as_ref(), Some(vec![Coin::new(50, "uandr")])) .unwrap(); let amp_pkt = AMPPkt::new( MOCK_CONTRACT_ADDR.to_string(), @@ -365,22 +353,14 @@ fn test_execute_send() { let amp_msg = amp_pkt .to_sub_msg( MOCK_KERNEL_CONTRACT, - Some(vec![Coin::new(41, "uandr"), Coin::new(38, "uandr")]), + Some(vec![Coin::new(50, "uandr"), Coin::new(50, "uandr")]), 1, ) .unwrap(); let expected_res = Response::new() - .add_submessages(vec![ - SubMsg::new( - // refunds remainder to sender - CosmosMsg::Bank(BankMsg::Send { - to_address: OWNER.to_string(), - amount: vec![Coin::new(21, "uandr")], // 100 - 41 - 38 remainder - }), - ), - amp_msg, - ]) + // No refund for the sender since the percentages add up to 100 + .add_submessage(amp_msg) .add_attributes(vec![attr("action", "send"), attr("sender", "creator")]) .add_submessage(generate_economics_message(OWNER, "Send")); diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 5ce907d31..cd11e97f1 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -31,18 +31,15 @@ impl Threshold { pub fn get_threshold( thresholds: &[Threshold], amount: Uint128, -) -> Result<(Threshold, usize), ContractError> { - // Create a vector of tuples containing the original index and the threshold - let mut indexed_thresholds: Vec<(usize, &Threshold)> = thresholds.iter().enumerate().collect(); +) -> Result { + let mut sorted_thresholds = thresholds.to_vec(); + // Sort the thresholds in decreasing order + sorted_thresholds.sort_by(|a, b| b.min.cmp(&a.min)); - // Sort thresholds by min values in decreasing order - indexed_thresholds.sort_by(|a, b| b.1.min.cmp(&a.1.min)); - - // Iterate over the sorted indexed thresholds - for (index, threshold) in indexed_thresholds { + for threshold in sorted_thresholds.into_iter() { + // Return the first threshold that's in range of the given amount if threshold.in_range(amount) { - // Return the threshold and its original index - return Ok((threshold.clone(), index)); + return Ok(threshold); } } Err(ContractError::InvalidRange {}) @@ -89,7 +86,7 @@ pub enum ExecuteMsg { #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { - /// The current config of the Splitter contract + /// The current config of the Conditional Splitter contract #[returns(GetConditionalSplitterConfigResponse)] GetConditionalSplitterConfig {}, } @@ -99,13 +96,12 @@ pub struct GetConditionalSplitterConfigResponse { pub config: ConditionalSplitter, } -/// Ensures that a given list of recipients for a `conditional splitter` contract is valid: +/// Ensures that a given list of thresholds is valid: /// * Percentages of each threshold should not exceed 100 /// * Each threshold must include at least one recipient /// * The number of recipients for each threshold must not exceed 100 /// * The recipient addresses must be unique for each threshold /// * Make sure there are no duplicate min values between the thresholds - pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<(), ContractError> { let mut min_value_set = HashSet::new(); @@ -261,7 +257,7 @@ mod tests { Uint128::one(), vec![AddressPercent::new( Recipient::new(AndrAddr::from_string("recipient"), None), - Decimal::zero(), + Decimal::one(), )], ), ], From f1133e7a9802914f3961eb1f5c014846a9c93d95 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 14:05:05 +0300 Subject: [PATCH 14/28] test: conditional splitter integration test --- .../src/mock.rs | 30 ++-- .../tests/conditional_splitter.rs | 150 ++++++++++++++++++ 2 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 tests-integration/tests/conditional_splitter.rs diff --git a/contracts/finance/andromeda-conditional-splitter/src/mock.rs b/contracts/finance/andromeda-conditional-splitter/src/mock.rs index 1ed3bd44a..16d9580f5 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/mock.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/mock.rs @@ -1,7 +1,7 @@ #![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] use crate::contract::{execute, instantiate, query, reply}; -use andromeda_finance::splitter::{AddressPercent, ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_finance::conditional_splitter::{ExecuteMsg, InstantiateMsg, QueryMsg, Threshold}; use andromeda_std::common::Milliseconds; use andromeda_testing::{ mock::MockApp, mock_ado, mock_contract::ExecuteResult, MockADO, MockContract, @@ -9,21 +9,29 @@ use andromeda_testing::{ use cosmwasm_std::{Addr, Coin, Empty}; use cw_multi_test::{Contract, ContractWrapper, Executor}; -pub struct MockSplitter(Addr); -mock_ado!(MockSplitter, ExecuteMsg, QueryMsg); +pub struct MockConditionalSplitter(Addr); +mock_ado!(MockConditionalSplitter, ExecuteMsg, QueryMsg); -impl MockSplitter { +impl MockConditionalSplitter { pub fn instantiate( app: &mut MockApp, code_id: u64, sender: Addr, - recipients: Vec, + thresholds: Vec, kernel_address: impl Into, lock_time: Option, owner: Option, ) -> Self { - let msg = mock_splitter_instantiate_msg(recipients, kernel_address, lock_time, owner); - let res = app.instantiate_contract(code_id, sender, &msg, &[], "Andromeda Splitter", None); + let msg = + mock_conditional_splitter_instantiate_msg(thresholds, kernel_address, lock_time, owner); + let res = app.instantiate_contract( + code_id, + sender, + &msg, + &[], + "Andromeda Conditional Splitter", + None, + ); Self(res.unwrap()) } @@ -35,19 +43,19 @@ impl MockSplitter { } } -pub fn mock_andromeda_splitter() -> Box> { +pub fn mock_andromeda_conditional_splitter() -> Box> { let contract = ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply); Box::new(contract) } -pub fn mock_splitter_instantiate_msg( - recipients: Vec, +pub fn mock_conditional_splitter_instantiate_msg( + thresholds: Vec, kernel_address: impl Into, lock_time: Option, owner: Option, ) -> InstantiateMsg { InstantiateMsg { - recipients, + thresholds, lock_time: lock_time.map(Milliseconds), kernel_address: kernel_address.into(), owner, diff --git a/tests-integration/tests/conditional_splitter.rs b/tests-integration/tests/conditional_splitter.rs new file mode 100644 index 000000000..939693f17 --- /dev/null +++ b/tests-integration/tests/conditional_splitter.rs @@ -0,0 +1,150 @@ +use andromeda_app::app::{AppComponent, ComponentType}; +use andromeda_app_contract::mock::{mock_andromeda_app, MockAppContract}; + +use andromeda_testing::{mock::mock_app, mock_builder::MockAndromedaBuilder, MockContract}; + +use andromeda_std::amp::Recipient; +use cosmwasm_std::{coin, Decimal, Uint128}; + +use andromeda_conditional_splitter::mock::{ + mock_andromeda_conditional_splitter, mock_conditional_splitter_instantiate_msg, + MockConditionalSplitter, +}; +use andromeda_finance::{conditional_splitter::Threshold, splitter::AddressPercent}; + +use std::str::FromStr; + +#[test] +fn test_conditional_splitter() { + let mut router = mock_app(None); + let andr = MockAndromedaBuilder::new(&mut router, "admin") + .with_wallets(vec![ + ("owner", vec![coin(100_000, "uandr")]), + ("recipient1", vec![]), + ("recipient2", vec![]), + ("recipient3", vec![]), + ]) + .with_contracts(vec![ + ("app-contract", mock_andromeda_app()), + ( + "conditional-splitter", + mock_andromeda_conditional_splitter(), + ), + ]) + .build(&mut router); + let owner = andr.get_wallet("owner"); + let recipient_1 = andr.get_wallet("recipient1"); + let recipient_2 = andr.get_wallet("recipient2"); + let recipient_3 = andr.get_wallet("recipient3"); + + let app_code_id = andr.get_code_id(&mut router, "app-contract"); + + let splitter_recipients = vec![ + AddressPercent { + recipient: Recipient::from_string(recipient_1.to_string()), + percent: Decimal::from_str("0.2").unwrap(), + }, + AddressPercent { + recipient: Recipient::from_string(recipient_2.to_string()), + percent: Decimal::from_str("0.8").unwrap(), + }, + ]; + + // Percentages that don't add up to 100 + let splitter_recipients3 = vec![ + AddressPercent { + recipient: Recipient::from_string(recipient_1.to_string()), + percent: Decimal::from_str("0.2").unwrap(), + }, + AddressPercent { + recipient: Recipient::from_string(recipient_2.to_string()), + percent: Decimal::from_str("0.5").unwrap(), + }, + AddressPercent { + recipient: Recipient::from_string(recipient_3.to_string()), + percent: Decimal::from_str("0.2").unwrap(), + }, + ]; + + let thresholds = vec![ + Threshold::new(Uint128::zero(), splitter_recipients.clone()), + Threshold::new(Uint128::new(10_000), splitter_recipients), + Threshold::new(Uint128::new(20_000), splitter_recipients3), + ]; + + let splitter_init_msg = mock_conditional_splitter_instantiate_msg( + thresholds, + andr.kernel.addr().clone(), + None, + None, + ); + let splitter_app_component = AppComponent { + name: "conditional-splitter".to_string(), + component_type: ComponentType::new(splitter_init_msg), + ado_type: "conditional-splitter".to_string(), + }; + + let app_components = vec![splitter_app_component.clone()]; + let app = MockAppContract::instantiate( + app_code_id, + owner, + &mut router, + "Conditional Splitter App", + app_components, + andr.kernel.addr(), + None, + ); + + let splitter: MockConditionalSplitter = + app.query_ado_by_component_name(&router, splitter_app_component.name); + + let token = coin(1000, "uandr"); + splitter + .execute_send(&mut router, owner.clone(), &[token]) + .unwrap(); + + let balance_1 = router.wrap().query_balance(recipient_1, "uandr").unwrap(); + let balance_2 = router.wrap().query_balance(recipient_2, "uandr").unwrap(); + + assert_eq!(balance_1.amount, Uint128::from(200u128)); + assert_eq!(balance_2.amount, Uint128::from(800u128)); + + // Second batch + let token2 = coin(10_000, "uandr"); + splitter + .execute_send(&mut router, owner.clone(), &[token2]) + .unwrap(); + + let balance_1 = router.wrap().query_balance(recipient_1, "uandr").unwrap(); + let balance_2 = router.wrap().query_balance(recipient_2, "uandr").unwrap(); + + assert_eq!(balance_1.amount, Uint128::from(200u128 + 2000u128)); + assert_eq!(balance_2.amount, Uint128::from(800u128 + 8000u128)); + + // Third batch + let token2 = coin(50_000, "uandr"); + splitter + .execute_send(&mut router, owner.clone(), &[token2]) + .unwrap(); + + let balance_1 = router.wrap().query_balance(recipient_1, "uandr").unwrap(); + let balance_2 = router.wrap().query_balance(recipient_2, "uandr").unwrap(); + let balance_3 = router.wrap().query_balance(recipient_3, "uandr").unwrap(); + + assert_eq!( + balance_1.amount, + Uint128::from(200u128 + 2000u128 + 10_000u128) + ); + assert_eq!( + balance_2.amount, + Uint128::from(800u128 + 8000u128 + 25_000u128) + ); + assert_eq!(balance_3.amount, Uint128::from(10_000u128)); + + let balance_owner = router.wrap().query_balance(owner, "uandr").unwrap(); + // First batch was 1000, second batch was 10,000 and both percentages added up to 100, the third batch was 50,000 but the percentages added up to 90, so 45,000 should have been deducted from his balance + assert_eq!( + balance_owner.amount, + Uint128::from(100_000u128 - 1000u128 - 10_000u128 - 45_000u128) + ); +} From 024b7df64b9480cf2cfddf7a17f9f725db1a6661 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 14:19:44 +0300 Subject: [PATCH 15/28] test: test case for unmet threshold --- .../src/testing/tests.rs | 62 +++++++++++++++++++ .../src/conditional_splitter.rs | 4 +- packages/std/src/error.rs | 4 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 742e43796..b848cea88 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -367,6 +367,68 @@ fn test_execute_send() { assert_eq!(res, expected_res); } +#[test] +fn test_execute_send_threshold_not_found() { + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); + let recip_address1 = "address1".to_string(); + let recip_address2 = "address2".to_string(); + let second_threshold = Uint128::new(10); + let recip1 = Recipient::from_string(recip_address1); + let recip2 = Recipient::from_string(recip_address2); + let msg = InstantiateMsg { + owner: Some(OWNER.to_owned()), + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + thresholds: vec![ + Threshold::new( + Uint128::new(7), + vec![ + AddressPercent::new( + recip1.clone(), // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + AddressPercent::new( + recip2.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + ], + ), + Threshold::new( + second_threshold, + vec![ + AddressPercent::new( + recip1.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + AddressPercent::new( + recip2.clone(), // 10% + Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + ), + ], + ), + ], + lock_time: Some(Milliseconds::from_seconds(100_000)), + }; + + let info = mock_info("owner", &[]); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // This batch is lower than the lowest threshold which is 7 + let first_batch = 6u128; + + // First batch + let info = mock_info(OWNER, &[Coin::new(first_batch, "uandr")]); + let msg = ExecuteMsg::Send {}; + let err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + + assert_eq!( + err, + ContractError::InvalidAmount { + msg: "The amount sent does not meet any threshold".to_string(), + } + ); +} + // #[test] // fn test_execute_send_ado_recipient() { // let mut deps = mock_dependencies_custom(&[]); diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index cd11e97f1..caa12a1db 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -42,7 +42,9 @@ pub fn get_threshold( return Ok(threshold); } } - Err(ContractError::InvalidRange {}) + Err(ContractError::InvalidAmount { + msg: "The amount sent does not meet any threshold".to_string(), + }) } #[cw_serde] diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 6d77e6240..48f4b8204 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -44,8 +44,8 @@ pub enum ContractError { #[error("InvalidOrigin")] InvalidOrigin {}, - #[error("InvalidRange")] - InvalidRange {}, + #[error("InvalidAmount: {msg}")] + InvalidAmount { msg: String }, #[error("OverlappingRanges")] OverlappingRanges {}, From a12c2a7d8a2beef3a1110a9f92a74dcb62c7490b Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 14:25:06 +0300 Subject: [PATCH 16/28] fix: conditional splitter's schema was using regular splitter's messages --- .../examples/schema.rs | 2 +- ...on => andromeda-conditional-splitter.json} | 116 +++++++++------ .../schema/raw/execute.json | 22 +-- .../schema/raw/instantiate.json | 29 +++- .../schema/raw/query.json | 6 +- ...se_to_get_conditional_splitter_config.json | 133 ++++++++++++++++++ 6 files changed, 235 insertions(+), 73 deletions(-) rename contracts/finance/andromeda-conditional-splitter/schema/{andromeda-splitter.json => andromeda-conditional-splitter.json} (94%) create mode 100644 contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json diff --git a/contracts/finance/andromeda-conditional-splitter/examples/schema.rs b/contracts/finance/andromeda-conditional-splitter/examples/schema.rs index fb68c826a..29a173a2e 100644 --- a/contracts/finance/andromeda-conditional-splitter/examples/schema.rs +++ b/contracts/finance/andromeda-conditional-splitter/examples/schema.rs @@ -1,4 +1,4 @@ -use andromeda_finance::splitter::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_finance::conditional_splitter::{ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_schema::write_api; fn main() { diff --git a/contracts/finance/andromeda-conditional-splitter/schema/andromeda-splitter.json b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json similarity index 94% rename from contracts/finance/andromeda-conditional-splitter/schema/andromeda-splitter.json rename to contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json index 13f4006c7..e14b27a5b 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/andromeda-splitter.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json @@ -1,5 +1,5 @@ { - "contract_name": "andromeda-splitter", + "contract_name": "andromeda-conditional-splitter", "contract_version": "1.0.0", "idl_version": "1.0.0", "instantiate": { @@ -8,7 +8,7 @@ "type": "object", "required": [ "kernel_address", - "recipients" + "thresholds" ], "properties": { "kernel_address": { @@ -30,11 +30,11 @@ "null" ] }, - "recipients": { + "thresholds": { "description": "The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on their assigned percentage.", "type": "array", "items": { - "$ref": "#/definitions/AddressPercent" + "$ref": "#/definitions/Threshold" } } }, @@ -107,6 +107,29 @@ } }, "additionalProperties": false + }, + "Threshold": { + "type": "object", + "required": [ + "address_percent", + "min" + ], + "properties": { + "address_percent": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "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" } } }, @@ -130,7 +153,7 @@ "recipients": { "type": "array", "items": { - "$ref": "#/definitions/AddressPercent" + "$ref": "#/definitions/Recipient" } } }, @@ -403,22 +426,6 @@ "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" }, - "AddressPercent": { - "type": "object", - "required": [ - "percent", - "recipient" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - }, - "recipient": { - "$ref": "#/definitions/Recipient" - } - }, - "additionalProperties": false - }, "AndrAddr": { "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", "type": "string", @@ -443,10 +450,6 @@ } } }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, "IBCConfig": { "type": "object", "properties": { @@ -765,13 +768,13 @@ "title": "QueryMsg", "oneOf": [ { - "description": "The current config of the Splitter contract", + "description": "The current config of the Conditional Splitter contract", "type": "object", "required": [ - "get_splitter_config" + "get_conditional_splitter_config" ], "properties": { - "get_splitter_config": { + "get_conditional_splitter_config": { "type": "object", "additionalProperties": false } @@ -998,16 +1001,16 @@ }, "additionalProperties": false }, - "get_splitter_config": { + "get_conditional_splitter_config": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetSplitterConfigResponse", + "title": "GetConditionalSplitterConfigResponse", "type": "object", "required": [ "config" ], "properties": { "config": { - "$ref": "#/definitions/Splitter" + "$ref": "#/definitions/ConditionalSplitter" } }, "additionalProperties": false, @@ -1037,6 +1040,32 @@ "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" }, + "ConditionalSplitter": { + "description": "A config struct for a `Conditional Splitter` contract.", + "type": "object", + "required": [ + "lock", + "thresholds" + ], + "properties": { + "lock": { + "description": "The lock's expiration time", + "allOf": [ + { + "$ref": "#/definitions/Milliseconds" + } + ] + }, + "thresholds": { + "description": "The vector of thresholds which assign a percentage for a certain range of received funds", + "type": "array", + "items": { + "$ref": "#/definitions/Threshold" + } + } + }, + "additionalProperties": false + }, "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" @@ -1080,31 +1109,28 @@ }, "additionalProperties": false }, - "Splitter": { - "description": "A config struct for a `Splitter` contract.", + "Threshold": { "type": "object", "required": [ - "lock", - "recipients" + "address_percent", + "min" ], "properties": { - "lock": { - "description": "The lock's expiration time", - "allOf": [ - { - "$ref": "#/definitions/Milliseconds" - } - ] - }, - "recipients": { - "description": "The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on their assigned percentage.", + "address_percent": { "type": "array", "items": { "$ref": "#/definitions/AddressPercent" } + }, + "min": { + "$ref": "#/definitions/Uint128" } }, "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" } } }, diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json index ac72d4ab6..510258c26 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json @@ -18,7 +18,7 @@ "recipients": { "type": "array", "items": { - "$ref": "#/definitions/AddressPercent" + "$ref": "#/definitions/Recipient" } } }, @@ -291,22 +291,6 @@ "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" }, - "AddressPercent": { - "type": "object", - "required": [ - "percent", - "recipient" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - }, - "recipient": { - "$ref": "#/definitions/Recipient" - } - }, - "additionalProperties": false - }, "AndrAddr": { "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", "type": "string", @@ -331,10 +315,6 @@ } } }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, "IBCConfig": { "type": "object", "properties": { diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/instantiate.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/instantiate.json index 6ead203e7..17e9f71b2 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/raw/instantiate.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/instantiate.json @@ -4,7 +4,7 @@ "type": "object", "required": [ "kernel_address", - "recipients" + "thresholds" ], "properties": { "kernel_address": { @@ -26,11 +26,11 @@ "null" ] }, - "recipients": { + "thresholds": { "description": "The vector of recipients for the contract. Anytime a `Send` execute message is sent the amount sent will be divided amongst these recipients depending on their assigned percentage.", "type": "array", "items": { - "$ref": "#/definitions/AddressPercent" + "$ref": "#/definitions/Threshold" } } }, @@ -103,6 +103,29 @@ } }, "additionalProperties": false + }, + "Threshold": { + "type": "object", + "required": [ + "address_percent", + "min" + ], + "properties": { + "address_percent": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "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" } } } diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/query.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/query.json index c06bee85f..9a758348a 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/raw/query.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/query.json @@ -3,13 +3,13 @@ "title": "QueryMsg", "oneOf": [ { - "description": "The current config of the Splitter contract", + "description": "The current config of the Conditional Splitter contract", "type": "object", "required": [ - "get_splitter_config" + "get_conditional_splitter_config" ], "properties": { - "get_splitter_config": { + "get_conditional_splitter_config": { "type": "object", "additionalProperties": false } diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json new file mode 100644 index 000000000..6370f22ea --- /dev/null +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json @@ -0,0 +1,133 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetConditionalSplitterConfigResponse", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/ConditionalSplitter" + } + }, + "additionalProperties": false, + "definitions": { + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, + "AndrAddr": { + "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", + "type": "string", + "pattern": "(^((([A-Za-z0-9]+://)?([A-Za-z0-9.\\-_]{2,80}/)))?((~[a-z0-9]{2,}|(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?)$)|(^(~[a-z0-9]{2,}|/(lib|home))(/[A-Za-z0-9.\\-_]{2,80}?)*(/)?$)|(^[a-z0-9]{2,}$)|(^\\.(/[A-Za-z0-9.\\-_]{2,40}?)*(/)?$)" + }, + "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" + }, + "ConditionalSplitter": { + "description": "A config struct for a `Conditional Splitter` contract.", + "type": "object", + "required": [ + "lock", + "thresholds" + ], + "properties": { + "lock": { + "description": "The lock's expiration time", + "allOf": [ + { + "$ref": "#/definitions/Milliseconds" + } + ] + }, + "thresholds": { + "description": "The vector of thresholds which assign a percentage for a certain range of received funds", + "type": "array", + "items": { + "$ref": "#/definitions/Threshold" + } + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Milliseconds": { + "description": "Represents time in milliseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "Recipient": { + "description": "A simple struct used for inter-contract communication. The struct can be used in two ways:\n\n1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address\n\nThe `Binary` message can be any message that the contract at the resolved address can handle.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/AndrAddr" + }, + "ibc_recovery_address": { + "anyOf": [ + { + "$ref": "#/definitions/AndrAddr" + }, + { + "type": "null" + } + ] + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Threshold": { + "type": "object", + "required": [ + "address_percent", + "min" + ], + "properties": { + "address_percent": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "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" + } + } +} From ce929d7d6a5ced698e6bc8c75142f6565d0fb256 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 17:55:06 +0300 Subject: [PATCH 17/28] refactor: update thresholds execute msg --- .../src/contract.rs | 83 ++++++++++--------- .../src/testing/tests.rs | 12 +-- .../src/conditional_splitter.rs | 13 ++- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index fb34e2c05..1601d9754 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -1,7 +1,7 @@ use crate::state::CONDITIONAL_SPLITTER; use andromeda_finance::conditional_splitter::{ get_threshold, ConditionalSplitter, ExecuteMsg, GetConditionalSplitterConfigResponse, - InstantiateMsg, QueryMsg, + InstantiateMsg, QueryMsg, Threshold, }; use std::vec; @@ -37,7 +37,7 @@ pub fn instantiate( let mut conditional_splitter = ConditionalSplitter { thresholds: msg.thresholds.clone(), - lock: msg.lock_time.unwrap_or_default(), + lock: msg.lock_time, }; // Validate thresholds conditional_splitter.validate(deps.as_ref())?; @@ -54,10 +54,10 @@ pub fn instantiate( lock_time.seconds() <= ONE_YEAR, ContractError::LockTimeTooLong {} ); - conditional_splitter.lock = current_time.plus_milliseconds(lock_time); + conditional_splitter.lock = Some(current_time.plus_milliseconds(lock_time)); } None => { - conditional_splitter.lock = Milliseconds::default(); + conditional_splitter.lock = None; } } // Save kernel address after validating it @@ -117,7 +117,7 @@ pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result execute_update_recipients(ctx, recipients), + ExecuteMsg::UpdateThresholds { thresholds } => execute_update_thresholds(ctx, thresholds), ExecuteMsg::UpdateLock { lock_time } => execute_update_lock(ctx, lock_time), ExecuteMsg::Send {} => execute_send(ctx), _ => ADOContract::default().execute(ctx, msg), @@ -200,41 +200,42 @@ fn execute_send(ctx: ExecuteContext) -> Result { .add_attribute("sender", info.sender.to_string())) } -// fn execute_update_recipients( -// ctx: ExecuteContext, -// recipients: Vec, -// ) -> Result { -// let ExecuteContext { -// deps, info, env, .. -// } = ctx; +fn execute_update_thresholds( + ctx: ExecuteContext, + thresholds: Vec, +) -> Result { + let ExecuteContext { + deps, info, env, .. + } = ctx; -// nonpayable(&info)?; + nonpayable(&info)?; -// ensure!( -// ADOContract::default().is_owner_or_operator(deps.storage, info.sender.as_str())?, -// ContractError::Unauthorized {} -// ); + ensure!( + ADOContract::default().is_owner_or_operator(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); -// validate_recipient_list(deps.as_ref(), recipients.clone())?; + let conditional_splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; -// let mut splitter = SPLITTER.load(deps.storage)?; -// // Can't call this function while the lock isn't expired + // Can't call this function while the lock isn't expired + if let Some(conditional_splitter_lock) = conditional_splitter.lock { + ensure!( + conditional_splitter_lock.is_expired(&env.block), + ContractError::ContractLocked {} + ); + } -// ensure!( -// splitter.lock.is_expired(&env.block), -// ContractError::ContractLocked {} -// ); -// // Max 100 recipients -// ensure!( -// recipients.len() <= 100, -// ContractError::ReachedRecipientLimit {} -// ); + let updated_conditional_splitter = ConditionalSplitter { + thresholds, + lock: conditional_splitter.lock, + }; + // Validate the updated conditional splitter + updated_conditional_splitter.validate(deps.as_ref())?; -// splitter.recipients = recipients; -// SPLITTER.save(deps.storage, &splitter)?; + CONDITIONAL_SPLITTER.save(deps.storage, &updated_conditional_splitter)?; -// Ok(Response::default().add_attributes(vec![attr("action", "update_recipients")])) -// } + Ok(Response::default().add_attributes(vec![attr("action", "update_thresholds")])) +} fn execute_update_lock( ctx: ExecuteContext, @@ -251,14 +252,16 @@ fn execute_update_lock( ContractError::Unauthorized {} ); - let mut splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; + let mut conditional_splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; // Can't call this function while the lock isn't expired + if let Some(conditional_splitter_lock) = conditional_splitter.lock { + ensure!( + conditional_splitter_lock.is_expired(&env.block), + ContractError::ContractLocked {} + ); + } - ensure!( - splitter.lock.is_expired(&env.block), - ContractError::ContractLocked {} - ); // Get current time let current_time = Milliseconds::from_seconds(env.block.time.seconds()); @@ -277,9 +280,9 @@ fn execute_update_lock( // Set new lock time let new_expiration = current_time.plus_milliseconds(lock_time); - splitter.lock = new_expiration; + conditional_splitter.lock = Some(new_expiration); - CONDITIONAL_SPLITTER.save(deps.storage, &splitter)?; + CONDITIONAL_SPLITTER.save(deps.storage, &conditional_splitter)?; Ok(Response::default().add_attributes(vec![ attr("action", "update_lock"), diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index b848cea88..232780ab2 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -75,7 +75,7 @@ fn test_execute_update_lock() { // Start off with an expiration that's behind current time (expired) let splitter = ConditionalSplitter { - lock: Milliseconds::from_seconds(current_time - 1), + lock: Some(Milliseconds::from_seconds(current_time - 1)), thresholds: vec![Threshold { min: Uint128::zero(), address_percent: vec![], @@ -105,8 +105,8 @@ fn test_execute_update_lock() { //check result let splitter = CONDITIONAL_SPLITTER.load(deps.as_ref().storage).unwrap(); - assert!(!splitter.lock.is_expired(&env.block)); - assert_eq!(new_lock, splitter.lock); + assert!(!splitter.lock.unwrap().is_expired(&env.block)); + assert_eq!(new_lock, splitter.lock.unwrap()); } // #[test] @@ -543,7 +543,7 @@ fn test_handle_packet_exit_with_error_true() { let msg = ExecuteMsg::AMPReceive(pkt); let splitter = ConditionalSplitter { - lock: Milliseconds::default(), + lock: None, thresholds: vec![Threshold::new(Uint128::zero(), address_percent)], }; @@ -566,7 +566,7 @@ fn test_query_splitter() { let mut deps = mock_dependencies_custom(&[]); let env = mock_env(); let splitter = ConditionalSplitter { - lock: Milliseconds::default(), + lock: None, thresholds: vec![Threshold::new(Uint128::zero(), vec![])], }; @@ -622,7 +622,7 @@ fn test_execute_send_error() { let splitter = ConditionalSplitter { thresholds: vec![Threshold::new(Uint128::zero(), address_percent)], - lock: Milliseconds::default(), + lock: None, }; CONDITIONAL_SPLITTER diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index caa12a1db..8e1582db8 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -1,5 +1,4 @@ use andromeda_std::{ - amp::recipient::Recipient, andr_exec, andr_instantiate, andr_query, common::{MillisecondsDuration, MillisecondsExpiration}, error::ContractError, @@ -53,7 +52,7 @@ pub struct ConditionalSplitter { /// The vector of thresholds which assign a percentage for a certain range of received funds pub thresholds: Vec, /// The lock's expiration time - pub lock: MillisecondsExpiration, + pub lock: Option, } impl ConditionalSplitter { pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { @@ -73,8 +72,8 @@ pub struct InstantiateMsg { #[andr_exec] #[cw_serde] pub enum ExecuteMsg { - /// Update the recipients list. Only executable by the contract owner when the contract is not locked. - UpdateRecipients { recipients: Vec }, + /// Update the thresholds. Only executable by the contract owner when the contract is not locked. + UpdateThresholds { thresholds: Vec }, /// Used to lock/unlock the contract allowing the config to be updated. UpdateLock { // Milliseconds from current time @@ -154,11 +153,9 @@ pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<() #[cfg(test)] mod tests { - - use andromeda_std::amp::AndrAddr; - use cosmwasm_std::testing::mock_dependencies; - use super::*; + use andromeda_std::amp::{AndrAddr, Recipient}; + use cosmwasm_std::testing::mock_dependencies; struct TestThresholdValidation { name: &'static str, From 7b7eab23f2e69a62b864b6ee927593da7934ef9b Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 18:22:38 +0300 Subject: [PATCH 18/28] refactor: simplify lock_time handling in instantiation --- .../src/contract.rs | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 1601d9754..7c303a666 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -33,33 +33,29 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - let current_time = Milliseconds::from_seconds(env.block.time.seconds()); - let mut conditional_splitter = ConditionalSplitter { thresholds: msg.thresholds.clone(), lock: msg.lock_time, }; + + if let Some(lock_time) = msg.lock_time { + // New lock time can't be too short + ensure!( + lock_time.seconds() >= ONE_DAY, + ContractError::LockTimeTooShort {} + ); + // New lock time can't be too long + ensure!( + lock_time.seconds() <= ONE_YEAR, + ContractError::LockTimeTooLong {} + ); + let current_time = Milliseconds::from_seconds(env.block.time.seconds()); + conditional_splitter.lock = Some(current_time.plus_milliseconds(lock_time)); + } + // Validate thresholds conditional_splitter.validate(deps.as_ref())?; - match msg.lock_time { - Some(lock_time) => { - // New lock time can't be too short - ensure!( - lock_time.seconds() >= ONE_DAY, - ContractError::LockTimeTooShort {} - ); - // New lock time can't be too long - ensure!( - lock_time.seconds() <= ONE_YEAR, - ContractError::LockTimeTooLong {} - ); - conditional_splitter.lock = Some(current_time.plus_milliseconds(lock_time)); - } - None => { - conditional_splitter.lock = None; - } - } // Save kernel address after validating it CONDITIONAL_SPLITTER.save(deps.storage, &conditional_splitter)?; From 4023fed2866b92796af55073ec4d12e8b4812380 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 29 Apr 2024 18:36:44 +0300 Subject: [PATCH 19/28] test: update thresholds --- .../src/testing/tests.rs | 156 +++++++++++------- 1 file changed, 97 insertions(+), 59 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 232780ab2..6b6cd2125 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -109,72 +109,110 @@ fn test_execute_update_lock() { assert_eq!(new_lock, splitter.lock.unwrap()); } -// #[test] -// fn test_execute_update_recipients() { -// let mut deps = mock_dependencies_custom(&[]); -// let env = mock_env(); -// let _res = init(deps.as_mut()); +#[test] +fn test_execute_update_thresholds() { + let mut deps = mock_dependencies_custom(&[]); + let env = mock_env(); + let _res = init(deps.as_mut()); -// let splitter = ConditionalSplitter { -// recipients: vec![], -// lock: Milliseconds::from_seconds(0), -// thresholds: vec![Threshold::new(Uint128::zero())], -// }; + let recip_address1 = "address1".to_string(); + let recip_address2 = "address2".to_string(); + let recip1 = Recipient::from_string(recip_address1); + let recip2 = Recipient::from_string(recip_address2); -// CONDITIONAL_SPLITTER -// .save(deps.as_mut().storage, &splitter) -// .unwrap(); + let first_thresholds = vec![Threshold::new( + Uint128::zero(), + vec![ + AddressPercent::new( + recip1.clone(), // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + AddressPercent::new( + recip2.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + ], + )]; + let splitter = ConditionalSplitter { + lock: None, + thresholds: first_thresholds, + }; -// // Duplicate recipients -// let duplicate_recipients = vec![ -// AddressPercentages { -// recipient: Recipient::from_string(String::from("addr1")), -// percentages: vec![Decimal::percent(40)], -// }, -// AddressPercentages { -// recipient: Recipient::from_string(String::from("addr1")), -// percentages: vec![Decimal::percent(60)], -// }, -// ]; -// let msg = ExecuteMsg::UpdateRecipients { -// recipients: duplicate_recipients, -// }; + CONDITIONAL_SPLITTER + .save(deps.as_mut().storage, &splitter) + .unwrap(); -// let info = mock_info(OWNER, &[]); -// let res = execute(deps.as_mut(), env.clone(), info, msg); -// assert_eq!(ContractError::DuplicateRecipient {}, res.unwrap_err()); + // Duplicate recipients + let duplicate_recipients = vec![Threshold::new( + Uint128::zero(), + vec![ + AddressPercent::new( + recip1.clone(), // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + AddressPercent::new( + recip1.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + ], + )]; + let msg = ExecuteMsg::UpdateThresholds { + thresholds: duplicate_recipients, + }; -// let recipients = vec![ -// AddressFunds { -// recipient: Recipient::from_string(String::from("addr1")), -// percent: Decimal::percent(40), -// }, -// AddressFunds { -// recipient: Recipient::from_string(String::from("addr2")), -// percent: Decimal::percent(60), -// }, -// ]; -// let msg = ExecuteMsg::UpdateRecipients { -// recipients: recipients.clone(), -// }; + let info = mock_info(OWNER, &[]); + let res = execute(deps.as_mut(), env.clone(), info, msg); + assert_eq!(ContractError::DuplicateRecipient {}, res.unwrap_err()); + + let new_threshold = vec![ + Threshold::new( + Uint128::zero(), + vec![ + AddressPercent::new( + recip1.clone(), // 50% + Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + ), + AddressPercent::new( + recip2.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + ], + ), + Threshold::new( + Uint128::new(20), + vec![ + AddressPercent::new( + recip1.clone(), // 20% + Decimal::from_ratio(Uint128::one(), Uint128::new(5)), + ), + AddressPercent::new( + recip2.clone(), // 10% + Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + ), + ], + ), + ]; + let msg = ExecuteMsg::UpdateThresholds { + thresholds: new_threshold.clone(), + }; -// let info = mock_info("incorrect_owner", &[]); -// let res = execute(deps.as_mut(), env.clone(), info, msg.clone()); -// assert_eq!(ContractError::Unauthorized {}, res.unwrap_err()); - -// let info = mock_info(OWNER, &[]); -// let res = execute(deps.as_mut(), env, info, msg).unwrap(); -// assert_eq!( -// Response::default() -// .add_attributes(vec![attr("action", "update_recipients")]) -// .add_submessage(generate_economics_message(OWNER, "UpdateRecipients")), -// res -// ); + let info = mock_info("incorrect_owner", &[]); + let res = execute(deps.as_mut(), env.clone(), info, msg.clone()); + assert_eq!(ContractError::Unauthorized {}, res.unwrap_err()); -// //check result -// let splitter = CONDITIONAL_SPLITTER.load(deps.as_ref().storage).unwrap(); -// assert_eq!(splitter.recipients, recipients); -// } + let info = mock_info(OWNER, &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + Response::default() + .add_attributes(vec![attr("action", "update_thresholds")]) + .add_submessage(generate_economics_message(OWNER, "UpdateThresholds")), + res + ); + + //check result + let splitter = CONDITIONAL_SPLITTER.load(deps.as_ref().storage).unwrap(); + assert_eq!(splitter.thresholds, new_threshold); +} #[test] fn test_execute_send() { From f84c4eaab07b605ea15edeccdfc34de8c50278d7 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 30 Apr 2024 13:43:41 +0300 Subject: [PATCH 20/28] refactor: rename lock to lock_time for consistency --- .../src/contract.rs | 12 ++++++------ .../src/testing/tests.rs | 18 +++++++++--------- .../src/conditional_splitter.rs | 6 ++++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 7c303a666..72ea69804 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -35,7 +35,7 @@ pub fn instantiate( ) -> Result { let mut conditional_splitter = ConditionalSplitter { thresholds: msg.thresholds.clone(), - lock: msg.lock_time, + lock_time: msg.lock_time, }; if let Some(lock_time) = msg.lock_time { @@ -50,7 +50,7 @@ pub fn instantiate( ContractError::LockTimeTooLong {} ); let current_time = Milliseconds::from_seconds(env.block.time.seconds()); - conditional_splitter.lock = Some(current_time.plus_milliseconds(lock_time)); + conditional_splitter.lock_time = Some(current_time.plus_milliseconds(lock_time)); } // Validate thresholds @@ -214,7 +214,7 @@ fn execute_update_thresholds( let conditional_splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; // Can't call this function while the lock isn't expired - if let Some(conditional_splitter_lock) = conditional_splitter.lock { + if let Some(conditional_splitter_lock) = conditional_splitter.lock_time { ensure!( conditional_splitter_lock.is_expired(&env.block), ContractError::ContractLocked {} @@ -223,7 +223,7 @@ fn execute_update_thresholds( let updated_conditional_splitter = ConditionalSplitter { thresholds, - lock: conditional_splitter.lock, + lock_time: conditional_splitter.lock_time, }; // Validate the updated conditional splitter updated_conditional_splitter.validate(deps.as_ref())?; @@ -251,7 +251,7 @@ fn execute_update_lock( let mut conditional_splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; // Can't call this function while the lock isn't expired - if let Some(conditional_splitter_lock) = conditional_splitter.lock { + if let Some(conditional_splitter_lock) = conditional_splitter.lock_time { ensure!( conditional_splitter_lock.is_expired(&env.block), ContractError::ContractLocked {} @@ -276,7 +276,7 @@ fn execute_update_lock( // Set new lock time let new_expiration = current_time.plus_milliseconds(lock_time); - conditional_splitter.lock = Some(new_expiration); + conditional_splitter.lock_time = Some(new_expiration); CONDITIONAL_SPLITTER.save(deps.storage, &conditional_splitter)?; diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs index 6b6cd2125..e2783206f 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/tests.rs @@ -75,7 +75,7 @@ fn test_execute_update_lock() { // Start off with an expiration that's behind current time (expired) let splitter = ConditionalSplitter { - lock: Some(Milliseconds::from_seconds(current_time - 1)), + lock_time: Some(Milliseconds::from_seconds(current_time - 1)), thresholds: vec![Threshold { min: Uint128::zero(), address_percent: vec![], @@ -105,8 +105,8 @@ fn test_execute_update_lock() { //check result let splitter = CONDITIONAL_SPLITTER.load(deps.as_ref().storage).unwrap(); - assert!(!splitter.lock.unwrap().is_expired(&env.block)); - assert_eq!(new_lock, splitter.lock.unwrap()); + assert!(!splitter.lock_time.unwrap().is_expired(&env.block)); + assert_eq!(new_lock, splitter.lock_time.unwrap()); } #[test] @@ -134,7 +134,7 @@ fn test_execute_update_thresholds() { ], )]; let splitter = ConditionalSplitter { - lock: None, + lock_time: None, thresholds: first_thresholds, }; @@ -280,10 +280,10 @@ fn test_execute_send() { // First batch will test first threshold let first_batch = 8u128; - // Second batch will be used to test the second threshold + // Second batch will test the second threshold let second_batch = 10u128; - // Third batch will be used to test crossing a threshold + // Third batch will test the third threshold let third_batch = 100u128; // First batch @@ -581,7 +581,7 @@ fn test_handle_packet_exit_with_error_true() { let msg = ExecuteMsg::AMPReceive(pkt); let splitter = ConditionalSplitter { - lock: None, + lock_time: None, thresholds: vec![Threshold::new(Uint128::zero(), address_percent)], }; @@ -604,7 +604,7 @@ fn test_query_splitter() { let mut deps = mock_dependencies_custom(&[]); let env = mock_env(); let splitter = ConditionalSplitter { - lock: None, + lock_time: None, thresholds: vec![Threshold::new(Uint128::zero(), vec![])], }; @@ -660,7 +660,7 @@ fn test_execute_send_error() { let splitter = ConditionalSplitter { thresholds: vec![Threshold::new(Uint128::zero(), address_percent)], - lock: None, + lock_time: None, }; CONDITIONAL_SPLITTER diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index 8e1582db8..de788397e 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -9,7 +9,7 @@ use std::collections::HashSet; use crate::splitter::AddressPercent; -// The contract owner will input a vector of Threshold +// The threshold has a min value and a vector of recipients, each having a respective percentage #[cw_serde] pub struct Threshold { pub min: Uint128, @@ -22,11 +22,13 @@ impl Threshold { address_percent, } } + // Checks if the funds sent are equal or greater than the min value pub fn in_range(&self, num: Uint128) -> bool { num >= self.min } } +// To get the threshold that corresponds to the funds sent, we sort the thresholds by min value in decreasing order, and return first threshold where the funds and in range of its min value pub fn get_threshold( thresholds: &[Threshold], amount: Uint128, @@ -52,7 +54,7 @@ pub struct ConditionalSplitter { /// The vector of thresholds which assign a percentage for a certain range of received funds pub thresholds: Vec, /// The lock's expiration time - pub lock: Option, + pub lock_time: Option, } impl ConditionalSplitter { pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { From 5446ac75a86ec1eb75703d222d8c9806f43727e9 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 30 Apr 2024 15:17:43 +0300 Subject: [PATCH 21/28] feat: support the distribution of up to 5 distinct coins at a time --- .../src/contract.rs | 28 +++++++++---------- .../tests/conditional_splitter.rs | 28 ++++++++++++++++++- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 72ea69804..3f3278c1c 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -134,7 +134,7 @@ fn execute_send(ctx: ExecuteContext) -> Result { } ); ensure!( - info.funds.len() == 1, + info.funds.len() < 5, ContractError::ExceedsMaxAllowedCoins {} ); for coin in info.funds.clone() { @@ -155,27 +155,25 @@ fn execute_send(ctx: ExecuteContext) -> Result { let mut pkt = AMPPkt::from_ctx(ctx.amp_ctx, ctx.env.contract.address.to_string()); - // Find the relevant threshold - let threshold = get_threshold( - &conditional_splitter.thresholds, - remainder_funds.first().unwrap().amount, - )?; + for (i, coin) in info.funds.clone().iter().enumerate() { + // Find the relevant threshold + let threshold = get_threshold(&conditional_splitter.thresholds, coin.amount)?; + + for address_percent in threshold.address_percent { + let recipient_percent = address_percent.percent; + let mut vec_coin: Vec = Vec::new(); - for address_percent in threshold.address_percent { - let recipient_percent = address_percent.percent; - let mut vec_coin: Vec = Vec::new(); - for (i, coin) in info.funds.clone().iter().enumerate() { let mut recip_coin: Coin = coin.clone(); recip_coin.amount = coin.amount * recipient_percent; remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; vec_coin.push(recip_coin.clone()); amp_funds.push(recip_coin); - } - let amp_msg = address_percent - .recipient - .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; - pkt = pkt.add_message(amp_msg); + let amp_msg = address_percent + .recipient + .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; + pkt = pkt.add_message(amp_msg); + } } remainder_funds.retain(|x| x.amount > Uint128::zero()); diff --git a/tests-integration/tests/conditional_splitter.rs b/tests-integration/tests/conditional_splitter.rs index 939693f17..a4e0557ef 100644 --- a/tests-integration/tests/conditional_splitter.rs +++ b/tests-integration/tests/conditional_splitter.rs @@ -19,7 +19,7 @@ fn test_conditional_splitter() { let mut router = mock_app(None); let andr = MockAndromedaBuilder::new(&mut router, "admin") .with_wallets(vec![ - ("owner", vec![coin(100_000, "uandr")]), + ("owner", vec![coin(100_000, "uandr"), coin(100_000, "uusd")]), ("recipient1", vec![]), ("recipient2", vec![]), ("recipient3", vec![]), @@ -147,4 +147,30 @@ fn test_conditional_splitter() { balance_owner.amount, Uint128::from(100_000u128 - 1000u128 - 10_000u128 - 45_000u128) ); + + // Try sending 2 distinct coins + let uandr_token = coin(10_000, "uandr"); + let uusd_token = coin(100, "uusd"); + + splitter + .execute_send(&mut router, owner.clone(), &[uandr_token, uusd_token]) + .unwrap(); + + let uandr_balance_1 = router.wrap().query_balance(recipient_1, "uandr").unwrap(); + let uandr_balance_2 = router.wrap().query_balance(recipient_2, "uandr").unwrap(); + + let uusd_balance_1 = router.wrap().query_balance(recipient_1, "uusd").unwrap(); + let uusd_balance_2 = router.wrap().query_balance(recipient_2, "uusd").unwrap(); + + assert_eq!( + uandr_balance_1.amount, + Uint128::from(200u128 + 2000u128 + 10_000u128 + 2000u128) + ); + assert_eq!( + uandr_balance_2.amount, + Uint128::from(800u128 + 8000u128 + 25_000u128 + 8000u128) + ); + + assert_eq!(uusd_balance_1.amount, Uint128::from(20u128)); + assert_eq!(uusd_balance_2.amount, Uint128::from(80u128)); } From 112aea22b1716ce64f78f3c1601c6d1db31c6148 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 7 May 2024 07:59:28 -0600 Subject: [PATCH 22/28] chore: conditional splitter changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b10b7a6..abe1bdd65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `ADOBaseVersion` query to all ADOs [(#416)](https://github.com/andromedaprotocol/andromeda-core/pull/416) - Staking: Added ability to remove/replace reward token [(#418)](https://github.com/andromedaprotocol/andromeda-core/pull/418) - Added Expiry Enum [(#419)](https://github.com/andromedaprotocol/andromeda-core/pull/419) +- Added Conditional Splitter [(#441)](https://github.com/andromedaprotocol/andromeda-core/pull/441) ### Changed From be893fe07749d73f4104cc3346bb703c65ac2459 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Mon, 13 May 2024 11:49:03 -0600 Subject: [PATCH 23/28] refactor: add non-empty check for validate_thresholds, add non-zero check for recipient's percentage, EmptyClassId error adjustment, add EmptyThresholdsList error --- .../src/contract.rs | 30 +++++++++------ .../src/conditional_splitter.rs | 37 ++++++++++++++++++- packages/std/src/error.rs | 5 ++- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 3f3278c1c..796c268b1 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -161,18 +161,24 @@ fn execute_send(ctx: ExecuteContext) -> Result { for address_percent in threshold.address_percent { let recipient_percent = address_percent.percent; - let mut vec_coin: Vec = Vec::new(); - - let mut recip_coin: Coin = coin.clone(); - recip_coin.amount = coin.amount * recipient_percent; - remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; - vec_coin.push(recip_coin.clone()); - amp_funds.push(recip_coin); - - let amp_msg = address_percent - .recipient - .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; - pkt = pkt.add_message(amp_msg); + + // Non-zero checks have already been made for the incoming coins, so we check if the recipient's percentage is above zero. + if !recipient_percent.is_zero() { + let mut vec_coin: Vec = Vec::new(); + + let mut recip_coin: Coin = coin.clone(); + recip_coin.amount = coin.amount * recipient_percent; + + remainder_funds[i].amount = + remainder_funds[i].amount.checked_sub(recip_coin.amount)?; + vec_coin.push(recip_coin.clone()); + amp_funds.push(recip_coin); + + let amp_msg = address_percent + .recipient + .generate_amp_msg(&deps.as_ref(), Some(vec_coin))?; + pkt = pkt.add_message(amp_msg); + } } } diff --git a/packages/andromeda-finance/src/conditional_splitter.rs b/packages/andromeda-finance/src/conditional_splitter.rs index de788397e..dea72d50e 100644 --- a/packages/andromeda-finance/src/conditional_splitter.rs +++ b/packages/andromeda-finance/src/conditional_splitter.rs @@ -100,14 +100,18 @@ pub struct GetConditionalSplitterConfigResponse { } /// Ensures that a given list of thresholds is valid: +/// * The list of thresholds is not empty /// * Percentages of each threshold should not exceed 100 /// * Each threshold must include at least one recipient /// * The number of recipients for each threshold must not exceed 100 /// * The recipient addresses must be unique for each threshold /// * Make sure there are no duplicate min values between the thresholds pub fn validate_thresholds(deps: Deps, thresholds: &Vec) -> Result<(), ContractError> { + ensure!( + !thresholds.is_empty(), + ContractError::EmptyThresholdsList {} + ); let mut min_value_set = HashSet::new(); - for threshold in thresholds { // Make sure the threshold has recipients ensure!( @@ -168,6 +172,11 @@ mod tests { #[test] fn test_validate_thresholds() { let test_cases = vec![ + TestThresholdValidation { + name: "Empty thresholds list", + thresholds: vec![], + expected_error: Some(ContractError::EmptyThresholdsList {}), + }, TestThresholdValidation { name: "Duplicate minimums between thresholds", thresholds: vec![ @@ -264,6 +273,32 @@ mod tests { ], expected_error: None, }, + TestThresholdValidation { + name: "Thresholds start above zero", + thresholds: vec![ + Threshold::new( + Uint128::new(20), + vec![ + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::zero(), + ), + AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient2"), None), + Decimal::new(Uint128::new(20)), + ), + ], + ), + Threshold::new( + Uint128::new(50), + vec![AddressPercent::new( + Recipient::new(AndrAddr::from_string("recipient"), None), + Decimal::one(), + )], + ), + ], + expected_error: None, + }, ]; for test in test_cases { diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 6e23811ea..05536c725 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -106,7 +106,7 @@ pub enum ContractError { #[error("EmptyOptional")] EmptyOptional {}, - #[error("EmptyOptional")] + #[error("EmptyClassId")] EmptyClassId {}, #[error("NoTokens")] @@ -208,6 +208,9 @@ pub enum ContractError { #[error("EmptyRecipientsList")] EmptyRecipientsList {}, + #[error("EmptyThresholdsList")] + EmptyThresholdsList {}, + #[error("AmountExceededHundredPrecent")] AmountExceededHundredPrecent {}, From e2ccee5326daf3e7d0fc5f56d5208faee3c0ef11 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 14 May 2024 10:46:10 -0600 Subject: [PATCH 24/28] fix: allow owner to update lock anytime, check amount owed to recipient before creating amp msg --- .../src/contract.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 796c268b1..3da8fbbe6 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -161,13 +161,13 @@ fn execute_send(ctx: ExecuteContext) -> Result { for address_percent in threshold.address_percent { let recipient_percent = address_percent.percent; + let amount_owed = coin.amount * recipient_percent; - // Non-zero checks have already been made for the incoming coins, so we check if the recipient's percentage is above zero. - if !recipient_percent.is_zero() { + if !amount_owed.is_zero() { let mut vec_coin: Vec = Vec::new(); - let mut recip_coin: Coin = coin.clone(); - recip_coin.amount = coin.amount * recipient_percent; + + recip_coin.amount = amount_owed; remainder_funds[i].amount = remainder_funds[i].amount.checked_sub(recip_coin.amount)?; @@ -254,14 +254,6 @@ fn execute_update_lock( let mut conditional_splitter = CONDITIONAL_SPLITTER.load(deps.storage)?; - // Can't call this function while the lock isn't expired - if let Some(conditional_splitter_lock) = conditional_splitter.lock_time { - ensure!( - conditional_splitter_lock.is_expired(&env.block), - ContractError::ContractLocked {} - ); - } - // Get current time let current_time = Milliseconds::from_seconds(env.block.time.seconds()); From 7cfc6a7adf8bdb3d498cb200269293bcbab89bc1 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Tue, 14 May 2024 11:53:59 -0600 Subject: [PATCH 25/28] refactor: use .mul_floor instead of directly multiplying a Uint128 with a Decimal --- .../finance/andromeda-conditional-splitter/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index 3da8fbbe6..a11605482 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -161,7 +161,7 @@ fn execute_send(ctx: ExecuteContext) -> Result { for address_percent in threshold.address_percent { let recipient_percent = address_percent.percent; - let amount_owed = coin.amount * recipient_percent; + let amount_owed = coin.amount.mul_floor(recipient_percent); if !amount_owed.is_zero() { let mut vec_coin: Vec = Vec::new(); From fd80a397a5f4f24511cb00cd4f044ac83a9dd6ba Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Wed, 15 May 2024 14:30:26 -0600 Subject: [PATCH 26/28] refactor: check if packet has messages before adding it to msgs --- .../finance/andromeda-conditional-splitter/src/contract.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/src/contract.rs b/contracts/finance/andromeda-conditional-splitter/src/contract.rs index a11605482..345e3ef1b 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/contract.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/contract.rs @@ -191,8 +191,10 @@ fn execute_send(ctx: ExecuteContext) -> Result { }))); } let kernel_address = ADOContract::default().get_kernel_address(deps.as_ref().storage)?; - let distro_msg = pkt.to_sub_msg(kernel_address, Some(amp_funds), 1)?; - msgs.push(distro_msg); + if !pkt.messages.is_empty() { + let distro_msg = pkt.to_sub_msg(kernel_address, Some(amp_funds), 1)?; + msgs.push(distro_msg); + } Ok(Response::new() .add_submessages(msgs) From 091ecaf9f13da5bdfae3daf230fbd6b751092fba Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Thu, 16 May 2024 08:11:41 -0600 Subject: [PATCH 27/28] chore: update schema for conditional splitter --- .../andromeda-conditional-splitter.json | 131 +++++++++++++++--- .../schema/raw/execute.json | 88 ++++++++++-- ...se_to_get_conditional_splitter_config.json | 8 +- .../schema/raw/response_to_permissions.json | 35 ++++- 4 files changed, 230 insertions(+), 32 deletions(-) diff --git a/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json index e14b27a5b..4fc7d4394 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json @@ -138,22 +138,22 @@ "title": "ExecuteMsg", "oneOf": [ { - "description": "Update the recipients list. Only executable by the contract owner when the contract is not locked.", + "description": "Update the thresholds. Only executable by the contract owner when the contract is not locked.", "type": "object", "required": [ - "update_recipients" + "update_thresholds" ], "properties": { - "update_recipients": { + "update_thresholds": { "type": "object", "required": [ - "recipients" + "thresholds" ], "properties": { - "recipients": { + "thresholds": { "type": "array", "items": { - "$ref": "#/definitions/Recipient" + "$ref": "#/definitions/Threshold" } } }, @@ -426,6 +426,22 @@ "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" }, + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, "AndrAddr": { "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", "type": "string", @@ -450,6 +466,39 @@ } } }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Expiry": { + "description": "The Expiry type is used to define an expiry time using milliseconds\n\nThere are two types: 1. FromNow(Milliseconds) - The expiry time is relative to the current time 2. AtTime(Milliseconds) - The expiry time is absolute", + "oneOf": [ + { + "type": "object", + "required": [ + "from_now" + ], + "properties": { + "from_now": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + } + ] + }, "IBCConfig": { "type": "object", "properties": { @@ -497,7 +546,7 @@ "expiration": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -527,7 +576,7 @@ "blacklisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -552,7 +601,7 @@ "expiration": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -579,7 +628,7 @@ "whitelisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -757,6 +806,25 @@ } ] }, + "Threshold": { + "type": "object", + "required": [ + "address_percent", + "min" + ], + "properties": { + "address_percent": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "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" @@ -1044,15 +1112,17 @@ "description": "A config struct for a `Conditional Splitter` contract.", "type": "object", "required": [ - "lock", "thresholds" ], "properties": { - "lock": { + "lock_time": { "description": "The lock's expiration time", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" } ] }, @@ -1238,6 +1308,35 @@ "$ref": "#/definitions/PermissionInfo" }, "definitions": { + "Expiry": { + "description": "The Expiry type is used to define an expiry time using milliseconds\n\nThere are two types: 1. FromNow(Milliseconds) - The expiry time is relative to the current time 2. AtTime(Milliseconds) - The expiry time is absolute", + "oneOf": [ + { + "type": "object", + "required": [ + "from_now" + ], + "properties": { + "from_now": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + } + ] + }, "Milliseconds": { "description": "Represents time in milliseconds.", "type": "integer", @@ -1256,7 +1355,7 @@ "blacklisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -1281,7 +1380,7 @@ "expiration": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -1308,7 +1407,7 @@ "whitelisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json index 510258c26..56cc99cf2 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/execute.json @@ -3,22 +3,22 @@ "title": "ExecuteMsg", "oneOf": [ { - "description": "Update the recipients list. Only executable by the contract owner when the contract is not locked.", + "description": "Update the thresholds. Only executable by the contract owner when the contract is not locked.", "type": "object", "required": [ - "update_recipients" + "update_thresholds" ], "properties": { - "update_recipients": { + "update_thresholds": { "type": "object", "required": [ - "recipients" + "thresholds" ], "properties": { - "recipients": { + "thresholds": { "type": "array", "items": { - "$ref": "#/definitions/Recipient" + "$ref": "#/definitions/Threshold" } } }, @@ -291,6 +291,22 @@ "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" }, + "AddressPercent": { + "type": "object", + "required": [ + "percent", + "recipient" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + }, + "recipient": { + "$ref": "#/definitions/Recipient" + } + }, + "additionalProperties": false + }, "AndrAddr": { "description": "An address that can be used within the Andromeda ecosystem. Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33\n\nThis address can be one of two things: 1. A valid human readable address e.g. `cosmos1...` 2. A valid Andromeda VFS path e.g. `/home/user/app/component`\n\nVFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:\n\n`:///` or `ibc://cosmoshub-4/user/app/component`", "type": "string", @@ -315,6 +331,39 @@ } } }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Expiry": { + "description": "The Expiry type is used to define an expiry time using milliseconds\n\nThere are two types: 1. FromNow(Milliseconds) - The expiry time is relative to the current time 2. AtTime(Milliseconds) - The expiry time is absolute", + "oneOf": [ + { + "type": "object", + "required": [ + "from_now" + ], + "properties": { + "from_now": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + } + ] + }, "IBCConfig": { "type": "object", "properties": { @@ -362,7 +411,7 @@ "expiration": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -392,7 +441,7 @@ "blacklisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -417,7 +466,7 @@ "expiration": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -444,7 +493,7 @@ "whitelisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -622,6 +671,25 @@ } ] }, + "Threshold": { + "type": "object", + "required": [ + "address_percent", + "min" + ], + "properties": { + "address_percent": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressPercent" + } + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "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" diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json index 6370f22ea..fc16ee416 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_get_conditional_splitter_config.json @@ -41,15 +41,17 @@ "description": "A config struct for a `Conditional Splitter` contract.", "type": "object", "required": [ - "lock", "thresholds" ], "properties": { - "lock": { + "lock_time": { "description": "The lock's expiration time", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/Milliseconds" + }, + { + "type": "null" } ] }, diff --git a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json index 545578ae5..dd1f735ba 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/raw/response_to_permissions.json @@ -6,6 +6,35 @@ "$ref": "#/definitions/PermissionInfo" }, "definitions": { + "Expiry": { + "description": "The Expiry type is used to define an expiry time using milliseconds\n\nThere are two types: 1. FromNow(Milliseconds) - The expiry time is relative to the current time 2. AtTime(Milliseconds) - The expiry time is absolute", + "oneOf": [ + { + "type": "object", + "required": [ + "from_now" + ], + "properties": { + "from_now": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Milliseconds" + } + }, + "additionalProperties": false + } + ] + }, "Milliseconds": { "description": "Represents time in milliseconds.", "type": "integer", @@ -24,7 +53,7 @@ "blacklisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -49,7 +78,7 @@ "expiration": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" @@ -76,7 +105,7 @@ "whitelisted": { "anyOf": [ { - "$ref": "#/definitions/Milliseconds" + "$ref": "#/definitions/Expiry" }, { "type": "null" From ded3b28c29842f66eb632525324e4404e3cc73b1 Mon Sep 17 00:00:00 2001 From: Joe Monem Date: Fri, 17 May 2024 08:41:22 -0600 Subject: [PATCH 28/28] chore: bumped conditional splitter version to 1.1.0 --- Cargo.lock | 2 +- contracts/finance/andromeda-conditional-splitter/Cargo.toml | 2 +- .../schema/andromeda-conditional-splitter.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2d2c4f35..9e271e0eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ [[package]] name = "andromeda-conditional-splitter" -version = "1.0.0" +version = "1.1.0" dependencies = [ "andromeda-app", "andromeda-finance", diff --git a/contracts/finance/andromeda-conditional-splitter/Cargo.toml b/contracts/finance/andromeda-conditional-splitter/Cargo.toml index 6ee00edb9..9c08de647 100644 --- a/contracts/finance/andromeda-conditional-splitter/Cargo.toml +++ b/contracts/finance/andromeda-conditional-splitter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "andromeda-conditional-splitter" -version = "1.0.0" +version = "1.1.0" edition = "2021" rust-version = "1.75.0" diff --git a/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json index 4fc7d4394..871ff03e9 100644 --- a/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json +++ b/contracts/finance/andromeda-conditional-splitter/schema/andromeda-conditional-splitter.json @@ -1,6 +1,6 @@ { "contract_name": "andromeda-conditional-splitter", - "contract_version": "1.0.0", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#",