diff --git a/Cargo.lock b/Cargo.lock index 9347da3ba..f72280a3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,12 +724,13 @@ dependencies = [ [[package]] name = "cw-abc" -version = "0.0.1" +version = "2.4.1" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-address-like", + "cw-curves", "cw-multi-test", "cw-ownable", "cw-paginate-storage 2.4.1", @@ -738,13 +739,13 @@ dependencies = [ "cw-utils 1.0.3", "cw2 1.1.2", "dao-interface", + "dao-proposal-single", "dao-testing", + "dao-voting 2.4.1", + "dao-voting-token-staked", "getrandom", - "integer-cbrt", - "integer-sqrt", "osmosis-std", "osmosis-test-tube", - "rust_decimal", "serde", "serde_json", "speculoos", @@ -886,6 +887,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cw-curves" +version = "2.4.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "integer-cbrt", + "integer-sqrt", + "rust_decimal", +] + [[package]] name = "cw-denom" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index bfdaec930..0ec1dd330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ cw-ownable = "0.5" cw-abc = { path = "./contracts/external/cw-abc", version = "*" } cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.4.1" } +cw-curves = { path = "./packages/cw-curves", version = "2.4.1" } cw-denom = { path = "./packages/cw-denom", version = "2.4.1" } cw-hooks = { path = "./packages/cw-hooks", version = "2.4.1" } cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.4.1" } diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index c1c321c91..83d6b82c9 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -183,7 +183,8 @@ fn main() -> Result<()> { ); // Persist contract code_ids in local.yaml so we can use SKIP_CONTRACT_STORE locally to avoid having to re-store them again - cfg.contract_deploy_info = orc.contract_map.deploy_info().clone(); + cfg.contract_deploy_info + .clone_from(orc.contract_map.deploy_info()); fs::write( "ci/configs/cosm-orc/local.yaml", serde_yaml::to_string(&cfg)?, diff --git a/ci/integration-tests/src/helpers/chain.rs b/ci/integration-tests/src/helpers/chain.rs index 20b3d27d4..fd7445cde 100644 --- a/ci/integration-tests/src/helpers/chain.rs +++ b/ci/integration-tests/src/helpers/chain.rs @@ -99,7 +99,8 @@ fn global_setup() -> Cfg { .unwrap(); save_gas_report(&orc, &gas_report_dir); // persist stored code_ids in CONFIG, so we can reuse for all tests - cfg.contract_deploy_info = orc.contract_map.deploy_info().clone(); + cfg.contract_deploy_info + .clone_from(orc.contract_map.deploy_info()); } Cfg { diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index 624208e82..4581a2a16 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -1723,7 +1723,7 @@ "additionalProperties": false }, { - "description": "Lists all of the items associted with the contract. For example, given the items `{ \"group\": \"foo\", \"subdao\": \"bar\"}` this query would return `[(\"group\", \"foo\"), (\"subdao\", \"bar\")]`.", + "description": "Lists all of the items associated with the contract. For example, given the items `{ \"group\": \"foo\", \"subdao\": \"bar\"}` this query would return `[(\"group\", \"foo\"), (\"subdao\", \"bar\")]`.", "type": "object", "required": [ "list_items" diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 43abe028c..3eb423ba0 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -4,13 +4,14 @@ authors = [ "Ethan Frey ", "Jake Hartnell", "Adair ", + "Gabe Lopez ", ] description = "Implements an Augmented Bonding Curve" # Inherits license from previous work license = "Apache-2.0" edition = { workspace = true } repository = { workspace = true } -version = "0.0.1" +version = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] @@ -36,11 +37,9 @@ cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } cw-tokenfactory-issuer = { workspace = true, features = ["library"] } dao-interface = { workspace = true } -rust_decimal = { workspace = true } -integer-sqrt = { workspace = true } -integer-cbrt = { workspace = true } getrandom = { workspace = true, features = ["js"] } thiserror = { workspace = true } +cw-curves = { workspace = true } [dev-dependencies] speculoos = { workspace = true } @@ -51,3 +50,7 @@ osmosis-std = { workspace = true } osmosis-test-tube = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +cw-tokenfactory-issuer = { workspace = true } +dao-voting-token-staked = { workspace = true } +dao-proposal-single = { workspace = true } +dao-voting = { workspace = true } \ No newline at end of file diff --git a/contracts/external/cw-abc/README.md b/contracts/external/cw-abc/README.md index 44ab82194..05d2c175e 100644 --- a/contracts/external/cw-abc/README.md +++ b/contracts/external/cw-abc/README.md @@ -1,6 +1,6 @@ # cw-abc -Implments an [Augmented Bonding Curve](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436). +Implements an [Augmented Bonding Curve](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436). Forked from and heavily inspired by the work on [cw20-bonding](https://github.com/cosmwasm/cw-tokens/tree/main/contracts/cw20-bonding). This contract uses native and token factory tokens instead. @@ -18,8 +18,8 @@ Each bonding curve has a pricing function, also known as the price curve (or `cu With bonding curves, we will always know what the price of an asset will be based on supply! More on benefits later. This contract implements two methods: -- `Buy {}` is called with sending along some reserve curency (such as $USDC, or whatever the bonding curve is backed by). The reserve currency is stored by the bonding curve contract, and new tokens are minted and sent to the user. -- `Sell {}` is called along with sending some supply currency (the token minted by the bonding curve). The supply tokens are burned, and reserve curency is returned. +- `Buy {}` is called with sending along some reserve currency (such as $USDC, or whatever the bonding curve is backed by). The reserve currency is stored by the bonding curve contract, and new tokens are minted and sent to the user. +- `Sell {}` is called along with sending some supply currency (the token minted by the bonding curve). The supply tokens are burned, and reserve currency is returned. It is possible to use this contact as a basic bonding curve, without any of the augmented features. @@ -64,7 +64,7 @@ Augmented Bonding Curves are nothing new, some articles that inspired this imple - https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436 - https://tokeneconomy.co/token-bonding-curves-in-practice-3eb904720cb8 -At a high level, augmented bonding curves extend bonding curves with new funcationality: +At a high level, augmented bonding curves extend bonding curves with new functionality: - Entry and exit fees - Different phases representing the life cycles of projects @@ -74,7 +74,7 @@ Example Instantiation message: ``` json { - "fees_recipient": "address that recieves fees", + "fees_recipient": "address that receives fees", "token_issuer_code_id": 0, "supply": { "subdenom": "utokenname", @@ -87,17 +87,17 @@ Example Instantiation message: "decimals": 6, "max_supply": "100000000000000" }, - reserve: { + "reserve": { "denom": "ujuno", "decimals": 6, }, - curve_type: { + "curve_type": { "linear": { "slope": "2", "scale": 1 } }, - phase_config: { + "phase_config": { "hatch": { "contribution_limits": { "min": "10000000", @@ -107,8 +107,7 @@ Example Instantiation message: "min": "10000000", "max": "100000000000" }, - "entry_fee": "0.25", - "exit_fee": "0.10" + "entry_fee": "0.25" }, "open": { "exit_fee": "0.01", @@ -116,13 +115,30 @@ Example Instantiation message: }, "closed": {} }, - hatcher_allowlist: ["allowlist addresses, leave blank for no allowlist"], + "hatcher_allowlist": [ + { + "addr": "dao_address", + "config": { + "config_type": { "dao": { "priority": 1 } }, + "contribution_limits_override": { + "min": "100000000", + "max": "99999999999999" + } + } + }, + { + "addr": "address", + "config": { + "config_type": { "address": {} } + } + } + ], } ``` -- `fees_recipient`: the address that will recieve fees (usually a DAO). +- `fees_recipient`: the address that will receive fees (usually a DAO). - `token_issuer_code_id`: the CosmWasm code ID for a `cw-tokenfactory_issuer` contract. -- `supply`: infor about the token that will be minted by the curve. This is the token that is created by the bonding curve. +- `supply`: info about the token that will be minted by the curve. This is the token that is created by the bonding curve. - `reserve`: this is the token that is used to mint the supply token. - `curve_type`: information about the pricing curve. - `phase_config`: configuration for the different phase of the augmented bonding curve. diff --git a/contracts/external/cw-abc/schema/cw-abc.json b/contracts/external/cw-abc/schema/cw-abc.json index 0a0187250..ffc6ec4d8 100644 --- a/contracts/external/cw-abc/schema/cw-abc.json +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -1,6 +1,6 @@ { "contract_name": "cw-abc", - "contract_version": "0.0.1", + "contract_version": "2.4.1", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,6 @@ "type": "object", "required": [ "curve_type", - "fees_recipient", "phase_config", "reserve", "supply", @@ -23,9 +22,12 @@ } ] }, - "fees_recipient": { - "description": "The recipient for any fees collected from bonding curve operation", - "type": "string" + "funding_pool_forwarding": { + "description": "An optional address for automatically forwarding funding pool gains", + "type": [ + "string", + "null" + ] }, "hatcher_allowlist": { "description": "TODO different ways of doing this, for example DAO members? Using a whitelist contract? Merkle tree? Hatcher allowlist", @@ -34,7 +36,7 @@ "null" ], "items": { - "type": "string" + "$ref": "#/definitions/HatcherAllowlistEntryMsg" } }, "phase_config": { @@ -234,7 +236,6 @@ "required": [ "contribution_limits", "entry_fee", - "exit_fee", "initial_raise" ], "properties": { @@ -254,27 +255,106 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/MinMax" } ] - }, - "initial_raise": { - "description": "The initial raise range (min, max) in the reserve token", + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigMsg": { + "type": "object", + "required": [ + "config_type" + ], + "properties": { + "config_type": { + "description": "The type of the configuration", "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ { "$ref": "#/definitions/MinMax" + }, + { + "type": "null" } ] } }, "additionalProperties": false }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntryMsg": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfigMsg" + } + }, + "additionalProperties": false + }, "MinMax": { - "description": "Struct for minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -418,6 +498,10 @@ "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" + }, + "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" } } }, @@ -454,7 +538,7 @@ "additionalProperties": false }, { - "description": "Donate will add reserve tokens to the funding pool", + "description": "Donate will donate tokens to the funding pool. You must send only reserve tokens.", "type": "object", "required": [ "donate" @@ -467,6 +551,33 @@ }, "additionalProperties": false }, + { + "description": "Withdraw will withdraw tokens from the funding pool.", + "type": "object", + "required": [ + "withdraw" + ], + "properties": { + "withdraw": { + "type": "object", + "properties": { + "amount": { + "description": "The amount to withdraw (defaults to full amount).", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Sets (or unsets if set to None) the maximum supply", "type": "object", @@ -517,7 +628,7 @@ "additionalProperties": false }, { - "description": "Update the hatch phase allowlist. This can only be called by the owner.", + "description": "Update the hatch phase allowlist. Only callable by owner.", "type": "object", "required": [ "update_hatch_allowlist" @@ -534,7 +645,7 @@ "description": "Addresses to be added.", "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/HatcherAllowlistEntryMsg" } }, "to_remove": { @@ -550,6 +661,43 @@ }, "additionalProperties": false }, + { + "description": "Toggles the paused state (circuit breaker)", + "type": "object", + "required": [ + "toggle_pause" + ], + "properties": { + "toggle_pause": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the funding pool forwarding. Only callable by owner.", + "type": "object", + "required": [ + "update_funding_pool_forwarding" + ], + "properties": { + "update_funding_pool_forwarding": { + "type": "object", + "properties": { + "address": { + "description": "The address to receive the funding pool forwarding. Set to None to stop forwarding.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Update the configuration of a certain phase. This can only be called by the owner.", "type": "object", @@ -782,8 +930,95 @@ } ] }, + "HatcherAllowlistConfigMsg": { + "type": "object", + "required": [ + "config_type" + ], + "properties": { + "config_type": { + "description": "The type of the configuration", + "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntryMsg": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfigMsg" + } + }, + "additionalProperties": false + }, "MinMax": { - "description": "Struct for minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -848,16 +1083,6 @@ } ] }, - "exit_fee": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, "initial_raise": { "anyOf": [ { @@ -1005,13 +1230,26 @@ "additionalProperties": false }, { - "description": "Returns the Fee Recipient for the contract. This is the address that recieves any fees collected from bonding curve operation", "type": "object", "required": [ - "fees_recipient" + "is_paused" + ], + "properties": { + "is_paused": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the funding pool forwarding config for the contract. This is the address that receives any fees collected from bonding curve operation and donations", + "type": "object", + "required": [ + "funding_pool_forwarding" ], "properties": { - "fees_recipient": { + "funding_pool_forwarding": { "type": "object", "additionalProperties": false } @@ -1049,7 +1287,69 @@ "additionalProperties": false }, { - "description": "Returns the Maxiumum Supply of the supply token", + "description": "Returns the contribution of a hatcher", + "type": "object", + "required": [ + "hatcher" + ], + "properties": { + "hatcher": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Lists the hatcher allowlist Returns [`HatcherAllowlistResponse`]", + "type": "object", + "required": [ + "hatcher_allowlist" + ], + "properties": { + "hatcher_allowlist": { + "type": "object", + "properties": { + "config_type": { + "anyOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + }, + { + "type": "null" + } + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the Maximum Supply of the supply token", "type": "object", "required": [ "max_supply" @@ -1062,6 +1362,50 @@ }, "additionalProperties": false }, + { + "description": "Returns the amount of tokens to receive from buying", + "type": "object", + "required": [ + "buy_quote" + ], + "properties": { + "buy_quote": { + "type": "object", + "required": [ + "payment" + ], + "properties": { + "payment": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the amount of tokens to receive from selling", + "type": "object", + "required": [ + "sell_quote" + ], + "properties": { + "sell_quote": { + "type": "object", + "required": [ + "payment" + ], + "properties": { + "payment": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns the current phase", "type": "object", @@ -1118,11 +1462,96 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "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" + }, + "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" + } + } }, "migrate": null, "sudo": null, "responses": { + "buy_quote": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QuoteResponse", + "type": "object", + "required": [ + "amount", + "funded", + "new_reserve", + "new_supply" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "funded": { + "$ref": "#/definitions/Uint128" + }, + "new_reserve": { + "$ref": "#/definitions/Uint128" + }, + "new_supply": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "curve_info": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "CurveInfoResponse", @@ -1332,12 +1761,174 @@ } } }, - "fees_recipient": { + "funding_pool_forwarding": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "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.", + "title": "Nullable_Addr", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, + "hatcher": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "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" }, + "hatcher_allowlist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HatcherAllowlistResponse", + "type": "object", + "properties": { + "allowlist": { + "description": "Hatcher allowlist", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/HatcherAllowlistEntry" + } + } + }, + "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" + }, + "HatcherAllowlistConfig": { + "description": "The configuration for a member of the hatcher allowlist", + "type": "object", + "required": [ + "config_height", + "config_type" + ], + "properties": { + "config_height": { + "description": "The height of the config insertion For use when checking allowlist of DAO configs", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "config_type": { + "description": "The type of the configuration", + "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntry": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfig" + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimum and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "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" + }, + "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" + } + } + }, "hatchers": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "HatchersResponse", @@ -1376,6 +1967,11 @@ } } }, + "is_paused": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "max_supply": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Uint128", @@ -1571,7 +2167,6 @@ "required": [ "contribution_limits", "entry_fee", - "exit_fee", "initial_raise" ], "properties": { @@ -1591,14 +2186,6 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "initial_raise": { "description": "The initial raise range (min, max) in the reserve token", "allOf": [ @@ -1611,7 +2198,7 @@ "additionalProperties": false }, "MinMax": { - "description": "Struct for minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -1659,6 +2246,38 @@ } } }, + "sell_quote": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QuoteResponse", + "type": "object", + "required": [ + "amount", + "funded", + "new_reserve", + "new_supply" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "funded": { + "$ref": "#/definitions/Uint128" + }, + "new_reserve": { + "$ref": "#/definitions/Uint128" + }, + "new_supply": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "token_contract": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Addr", diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 30f1f430b..1d86a26cb 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -1,8 +1,12 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Decimal as StdDecimal, Uint128}; +use cosmwasm_std::{ensure, Decimal, Uint128}; +use cw_curves::{ + curves::{Constant, Linear, SquareRoot}, + utils::decimal, + Curve, DecimalPlaces, +}; use dao_interface::token::NewDenomMetadata; -use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; use crate::ContractError; #[cw_serde] @@ -27,13 +31,15 @@ pub struct ReserveToken { pub decimals: u8, } -/// Struct for minimium and maximum values +/// Struct for minimum and maximum values #[cw_serde] pub struct MinMax { pub min: Uint128, pub max: Uint128, } +impl Copy for MinMax {} + #[cw_serde] pub struct HatchConfig { /// The minimum and maximum contribution amounts (min, max) in the reserve token @@ -41,11 +47,11 @@ pub struct HatchConfig { /// The initial raise range (min, max) in the reserve token pub initial_raise: MinMax, /// The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool - pub entry_fee: StdDecimal, - /// Exit tax for the hatch phase - pub exit_fee: StdDecimal, + pub entry_fee: Decimal, } +impl Copy for HatchConfig {} + impl HatchConfig { /// Validate the hatch config pub fn validate(&self) -> Result<(), ContractError> { @@ -57,27 +63,12 @@ impl HatchConfig { ); ensure!( - self.contribution_limits.max <= self.initial_raise.max, - ContractError::HatchPhaseConfigError( - "Max contribution limit cannot be greater than the maximum initial raise." - .to_string() - ) - ); - - ensure!( - self.entry_fee <= StdDecimal::percent(100u64), + self.entry_fee <= Decimal::percent(100u64), ContractError::HatchPhaseConfigError( "Initial allocation percentage must be between 0 and 100.".to_string() ) ); - ensure!( - self.exit_fee <= StdDecimal::percent(100u64), - ContractError::HatchPhaseConfigError( - "Exit taxation percentage must be less than or equal to 100.".to_string() - ) - ); - Ok(()) } } @@ -86,23 +77,23 @@ impl HatchConfig { pub struct OpenConfig { /// Percentage of capital put into the Reserve Pool during the Open phase /// when buying from the curve. - pub entry_fee: StdDecimal, + pub entry_fee: Decimal, /// Exit taxation ratio - pub exit_fee: StdDecimal, + pub exit_fee: Decimal, } impl OpenConfig { /// Validate the open config pub fn validate(&self) -> Result<(), ContractError> { ensure!( - self.entry_fee <= StdDecimal::percent(100u64), + self.entry_fee <= Decimal::percent(100u64), ContractError::OpenPhaseConfigError( "Reserve percentage must be between 0 and 100.".to_string() ) ); ensure!( - self.exit_fee <= StdDecimal::percent(100u64), + self.exit_fee <= Decimal::percent(100u64), ContractError::OpenPhaseConfigError( "Exit taxation percentage must be between 0 and 100.".to_string() ) @@ -226,39 +217,3 @@ impl CurveType { } } } - -#[cfg(test)] -mod unit_tests { - use super::*; - - #[test] - fn validate_contribution_limit_not_gt_initial_raise() { - let phase_config = CommonsPhaseConfig { - hatch: HatchConfig { - contribution_limits: MinMax { - min: Uint128::one(), - max: Uint128::MAX, - }, - initial_raise: MinMax { - min: Uint128::one(), - max: Uint128::from(1000000u128), - }, - entry_fee: StdDecimal::percent(10u64), - exit_fee: StdDecimal::percent(10u64), - }, - open: OpenConfig { - entry_fee: StdDecimal::percent(10u64), - exit_fee: StdDecimal::percent(10u64), - }, - closed: ClosedConfig {}, - }; - let err = phase_config.validate().unwrap_err(); - assert_eq!( - err, - ContractError::HatchPhaseConfigError( - "Max contribution limit cannot be greater than the maximum initial raise." - .to_string() - ) - ) - } -} diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 205f94cfc..6bb825e82 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,24 +1,23 @@ use cosmwasm_std::{ - ensure, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, - MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, Uint128, WasmMsg, + to_json_binary, Addr, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, QuerierWrapper, + Response, StdResult, Storage, Uint128, WasmMsg, }; use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; use cw_utils::must_pay; -use std::collections::HashSet; use std::ops::Deref; -use crate::abc::{CommonsPhase, CurveType}; -use crate::msg::UpdatePhaseConfigMsg; +use crate::abc::{CommonsPhase, CurveType, HatchConfig, MinMax}; +use crate::helpers::{calculate_buy_quote, calculate_sell_quote}; +use crate::msg::{HatcherAllowlistEntryMsg, UpdatePhaseConfigMsg}; use crate::state::{ - CURVE_STATE, CURVE_TYPE, DONATIONS, FEES_RECIPIENT, HATCHERS, HATCHER_ALLOWLIST, MAX_SUPPLY, - PHASE, PHASE_CONFIG, SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, + hatcher_allowlist, HatcherAllowlistConfig, HatcherAllowlistConfigType, CURVE_STATE, CURVE_TYPE, + DONATIONS, FUNDING_POOL_FORWARDING, HATCHERS, HATCHER_DAO_PRIORITY_QUEUE, IS_PAUSED, + MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, }; use crate::ContractError; -pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { +pub fn buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { let curve_type = CURVE_TYPE.load(deps.storage)?; - let curve_fn = curve_type.to_curve_fn(); - let mut curve_state = CURVE_STATE.load(deps.storage)?; let payment = must_pay(&info, &curve_state.reserve_denom)?; @@ -27,25 +26,30 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { - let hatch_config = phase_config.hatch; // Check that the potential hatcher is allowlisted - assert_allowlisted(deps.storage, &info.sender)?; + let hatch_config = assert_allowlisted( + deps.querier, + deps.storage, + &info.sender, + &phase_config.hatch, + )?; // Update hatcher contribution - let contribution = update_hatcher_contributions(deps.storage, &info.sender, payment)?; - - // Check contribtuion is above minimum - if contribution < hatch_config.contribution_limits.min { - return Err(ContractError::ContributionLimit { - min: hatch_config.contribution_limits.min, - max: hatch_config.contribution_limits.max, - }); - } - - // Check contribution is below maximum - if contribution > hatch_config.contribution_limits.max { + let contribution = + HATCHERS.update(deps.storage, &info.sender, |amount| -> StdResult<_> { + Ok(amount.unwrap_or_default() + payment) + })?; + + // Check contribution is within limits + if contribution < hatch_config.contribution_limits.min + || contribution > hatch_config.contribution_limits.max + { return Err(ContractError::ContributionLimit { min: hatch_config.contribution_limits.min, max: hatch_config.contribution_limits.max, @@ -53,113 +57,101 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result= hatch_config.initial_raise.max { + if buy_quote.new_reserve >= hatch_config.initial_raise.max { // Transition to the Open phase phase = CommonsPhase::Open; + + // Can clean up state here + hatcher_allowlist().clear(deps.storage); + PHASE.save(deps.storage, &phase)?; } - - calculate_reserved_and_funded(payment, hatch_config.entry_fee) } - CommonsPhase::Open => calculate_reserved_and_funded(payment, phase_config.open.entry_fee), + CommonsPhase::Open => {} CommonsPhase::Closed => { return Err(ContractError::CommonsClosed {}); } }; - // Calculate how many tokens can be purchased with this and mint them - let curve = curve_fn(curve_state.clone().decimals); - curve_state.reserve += reserved; - curve_state.funding += funded; - - // Calculate the supply based on the reserve - let new_supply = curve.supply(curve_state.reserve); - let minted = new_supply - .checked_sub(curve_state.supply) - .map_err(StdError::overflow)?; - // Check that the minted amount has not exceeded the max supply (if configured) if let Some(max_supply) = MAX_SUPPLY.may_load(deps.storage)? { - if new_supply > max_supply { + if buy_quote.new_supply > max_supply { return Err(ContractError::CannotExceedMaxSupply { max: max_supply }); } } - // Save the new curve state - curve_state.supply = new_supply; - CURVE_STATE.save(deps.storage, &curve_state)?; - // Mint tokens for sender by calling mint on the cw-tokenfactory-issuer contract let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; let mut msgs: Vec = vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: issuer_addr.to_string(), msg: to_json_binary(&IssuerExecuteMsg::Mint { to_address: info.sender.to_string(), - amount: minted, + amount: buy_quote.amount, })?, funds: vec![], })]; // Send funding to fee recipient - if funded > Uint128::zero() { - let fees_recipient = FEES_RECIPIENT.load(deps.storage)?; - msgs.push(CosmosMsg::Bank(BankMsg::Send { - to_address: fees_recipient.to_string(), - amount: vec![Coin { - amount: funded, - denom: curve_state.reserve_denom, - }], - })) + if buy_quote.funded > Uint128::zero() { + if let Some(funding_pool_forwarding) = FUNDING_POOL_FORWARDING.may_load(deps.storage)? { + msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: funding_pool_forwarding.to_string(), + amount: vec![Coin { + amount: buy_quote.funded, + denom: curve_state.reserve_denom.clone(), + }], + })) + } else { + curve_state.funding += buy_quote.funded; + } }; + // Save the new curve state + curve_state.supply = buy_quote.new_supply; + curve_state.reserve = buy_quote.new_reserve; + + CURVE_STATE.save(deps.storage, &curve_state)?; + Ok(Response::new() .add_messages(msgs) .add_attribute("action", "buy") .add_attribute("from", info.sender) - .add_attribute("reserved", reserved) - .add_attribute("funded", funded) - .add_attribute("supply", minted)) -} - -/// Return the reserved and funded amounts based on the payment and the allocation ratio -fn calculate_reserved_and_funded( - payment: Uint128, - allocation_ratio: StdDecimal, -) -> (Uint128, Uint128) { - let funded = payment * allocation_ratio; - let reserved = payment.checked_sub(funded).unwrap(); // Since allocation_ratio is < 1, this subtraction is safe - (reserved, funded) -} - -/// Add the hatcher's contribution to the total contributions -fn update_hatcher_contributions( - storage: &mut dyn Storage, - hatcher: &Addr, - contribution: Uint128, -) -> StdResult { - HATCHERS.update(storage, hatcher, |amount| -> StdResult<_> { - match amount { - Some(mut amount) => { - amount += contribution; - Ok(amount) - } - None => Ok(contribution), - } - }) + .add_attribute("amount", payment) + .add_attribute("reserved", buy_quote.new_reserve) + .add_attribute("minted", buy_quote.amount) + .add_attribute("funded", buy_quote.funded) + .add_attribute("supply", buy_quote.new_supply)) } /// Sell tokens on the bonding curve -pub fn execute_sell( - deps: DepsMut, - _env: Env, - info: MessageInfo, -) -> Result { +pub fn sell(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { let curve_type = CURVE_TYPE.load(deps.storage)?; - let curve_fn = curve_type.to_curve_fn(); - let supply_denom = SUPPLY_DENOM.load(deps.storage)?; let burn_amount = must_pay(&info, &supply_denom)?; + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + // Load the phase configuration and the current phase + let phase_config = PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + + // Calculate the sell quote + let sell_quote = calculate_sell_quote( + burn_amount, + &curve_type, + &curve_state, + &phase, + &phase_config, + )?; + + let mut send_msgs: Vec = vec![CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + amount: sell_quote.amount, + denom: curve_state.reserve_denom.clone(), + }], + })]; + let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; // Burn the sent supply tokens @@ -183,98 +175,40 @@ pub fn execute_sell( }), ]; - let mut curve_state = CURVE_STATE.load(deps.storage)?; - let curve = curve_fn(curve_state.clone().decimals); - - // Reduce the supply by the amount burned - curve_state.supply = curve_state - .supply - .checked_sub(burn_amount) - .map_err(StdError::overflow)?; - - // Calculate the new reserve based on the new supply - let new_reserve = curve.reserve(curve_state.supply); - - // Calculate how many reserve tokens to release based on the sell amount - let released_reserve = curve_state - .reserve - .checked_sub(new_reserve) - .map_err(StdError::overflow)?; - - // Calculate the exit tax - let taxed_amount = calculate_exit_fee(deps.storage, released_reserve)?; + // Send exit fee to the funding pool + if sell_quote.funded > Uint128::zero() { + if let Some(funding_pool_forwarding) = FUNDING_POOL_FORWARDING.may_load(deps.storage)? { + send_msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: funding_pool_forwarding.to_string(), + amount: vec![Coin { + amount: sell_quote.funded, + denom: curve_state.reserve_denom.clone(), + }], + })) + } else { + curve_state.funding += sell_quote.funded; + } + } // Update the curve state - curve_state.reserve = new_reserve; - curve_state.funding += taxed_amount; + curve_state.reserve = sell_quote.new_reserve; + curve_state.supply = sell_quote.new_supply; CURVE_STATE.save(deps.storage, &curve_state)?; - // Calculate the amount of tokens to send to the sender - // Subtract the taxed amount from the released amount - let released = released_reserve - .checked_sub(taxed_amount) - .map_err(StdError::overflow)?; - - // Now send the tokens to the sender and any fees to the DAO - let mut send_msgs: Vec = vec![CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin { - amount: released, - denom: curve_state.reserve_denom.clone(), - }], - })]; - - // Send exit fees to the to the fee recipient - if taxed_amount > Uint128::zero() { - let fees_recipient = FEES_RECIPIENT.load(deps.storage)?; - send_msgs.push(CosmosMsg::Bank(BankMsg::Send { - to_address: fees_recipient.to_string(), - amount: vec![Coin { - amount: taxed_amount, - denom: curve_state.reserve_denom, - }], - })) - } - Ok(Response::new() .add_messages(burn_msgs) .add_messages(send_msgs) - .add_attribute("action", "burn") + .add_attribute("action", "sell") .add_attribute("from", info.sender) .add_attribute("amount", burn_amount) - .add_attribute("burned", released_reserve) - .add_attribute("funded", taxed_amount)) -} - -/// Calculate the exit taxation for the sell amount based on the phase -fn calculate_exit_fee( - storage: &dyn Storage, - sell_amount: Uint128, -) -> Result { - // Load the phase config and phase - let phase = PHASE.load(storage)?; - let phase_config = PHASE_CONFIG.load(storage)?; - - // Calculate the exit tax based on the phase - let exit_fee = match &phase { - CommonsPhase::Hatch => phase_config.hatch.exit_fee, - CommonsPhase::Open => phase_config.open.exit_fee, - CommonsPhase::Closed => return Ok(Uint128::zero()), - }; - - // Ensure the exit fee is not greater than 100% - ensure!( - exit_fee <= StdDecimal::percent(100), - ContractError::InvalidExitFee {} - ); - - // This won't ever overflow because it's checked - let taxed_amount = sell_amount * exit_fee; - Ok(taxed_amount) + .add_attribute("reserved", sell_quote.new_reserve) + .add_attribute("supply", sell_quote.new_supply) + .add_attribute("burned", sell_quote.amount) + .add_attribute("funded", sell_quote.funded)) } /// Transitions the bonding curve to a closed phase where only sells are allowed -pub fn execute_close(deps: DepsMut, info: MessageInfo) -> Result { +pub fn close(deps: DepsMut, info: MessageInfo) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; PHASE.save(deps.storage, &CommonsPhase::Closed)?; @@ -283,42 +217,176 @@ pub fn execute_close(deps: DepsMut, info: MessageInfo) -> Result Result { + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + let payment = must_pay(&info, &curve_state.reserve_denom)?; + + let msgs = + if let Some(funding_pool_forwarding) = FUNDING_POOL_FORWARDING.may_load(deps.storage)? { + vec![CosmosMsg::Bank(BankMsg::Send { + to_address: funding_pool_forwarding.to_string(), + amount: info.funds, + })] + } else { + curve_state.funding += payment; + + CURVE_STATE.save(deps.storage, &curve_state)?; + + vec![] + }; + + // No minting of tokens is necessary, the supply stays the same + let total_donation = + DONATIONS.update(deps.storage, &info.sender, |maybe_amount| -> StdResult<_> { + if let Some(amount) = maybe_amount { + Ok(amount.checked_add(payment)?) + } else { + Ok(payment) + } + })?; + + Ok(Response::new() + .add_attribute("action", "donate") + .add_attribute("donor", info.sender) + .add_attribute("amount", payment) + .add_attribute("total_donation", total_donation) + .add_messages(msgs)) +} + +/// Withdraw funds from the funding pool (only callable by owner) +pub fn withdraw( deps: DepsMut, _env: Env, info: MessageInfo, + amount: Option, ) -> Result { + // Validate ownership + cw_ownable::assert_owner(deps.storage, &info.sender)?; + let mut curve_state = CURVE_STATE.load(deps.storage)?; - let payment = must_pay(&info, &curve_state.reserve_denom)?; - curve_state.funding += payment; + // Get amount to withdraw + let amount = amount.unwrap_or(curve_state.funding); + + // Construct the withdraw message + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: curve_state.reserve_denom.clone(), + amount, + }], + }); + + // Update the curve state + curve_state.funding = curve_state.funding.checked_sub(amount)?; CURVE_STATE.save(deps.storage, &curve_state)?; - // No minting of tokens is necessary, the supply stays the same - DONATIONS.save(deps.storage, &info.sender, &payment)?; + Ok(Response::new() + .add_attribute("action", "withdraw") + .add_attribute("withdrawer", info.sender) + .add_attribute("amount", amount) + .add_message(msg)) +} + +/// Updates the funding pool forwarding (only callable by owner) +pub fn update_funding_pool_forwarding( + deps: DepsMut, + env: Env, + info: MessageInfo, + address: Option, +) -> Result { + // Validate ownership + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // Update the funding pool forwarding + match &address { + Some(address) => { + FUNDING_POOL_FORWARDING.save(deps.storage, &deps.api.addr_validate(address)?)?; + } + None => FUNDING_POOL_FORWARDING.remove(deps.storage), + }; Ok(Response::new() - .add_attribute("action", "donate") - .add_attribute("donor", info.sender) - .add_attribute("amount", payment)) + .add_attribute("action", "update_funding_pool_forwarding") + .add_attribute( + "address", + address.unwrap_or(env.contract.address.to_string()), + )) } /// Check if the sender is allowlisted for the hatch phase -fn assert_allowlisted(storage: &dyn Storage, hatcher: &Addr) -> Result<(), ContractError> { - let allowlist = HATCHER_ALLOWLIST.may_load(storage)?; - if let Some(allowlist) = allowlist { - ensure!( - allowlist.contains(hatcher), - ContractError::SenderNotAllowlisted { - sender: hatcher.to_string(), +fn assert_allowlisted( + querier: QuerierWrapper, + storage: &dyn Storage, + hatcher: &Addr, + hatch_config: &HatchConfig, +) -> Result { + if !hatcher_allowlist().is_empty(storage) { + // Specific configs should trump everything + if hatcher_allowlist().has(storage, hatcher) { + let config = hatcher_allowlist().load(storage, hatcher)?; + + // Do not allow DAO's to purchase themselves when allowlisted as a DAO + if matches!( + config.config_type, + HatcherAllowlistConfigType::DAO { priority: _ } + ) { + return Err(ContractError::SenderNotAllowlisted { + sender: hatcher.to_string(), + }); } - ); + + return Ok(HatchConfig { + contribution_limits: config + .contribution_limits_override + .unwrap_or(hatch_config.contribution_limits), + ..*hatch_config + }); + } + + // If not allowlisted as individual, then check any DAO allowlists + return Ok(HatchConfig { + contribution_limits: assert_allowlisted_through_daos(querier, storage, hatcher)? + .unwrap_or(hatch_config.contribution_limits), + ..*hatch_config + }); } - Ok(()) + Ok(*hatch_config) } -/// Set the maxiumum supply (only callable by owner) +fn assert_allowlisted_through_daos( + querier: QuerierWrapper, + storage: &dyn Storage, + hatcher: &Addr, +) -> Result, ContractError> { + if let Some(hatcher_dao_priority_queue) = HATCHER_DAO_PRIORITY_QUEUE.may_load(storage)? { + for entry in hatcher_dao_priority_queue { + let voting_power_response_result: StdResult< + dao_interface::voting::VotingPowerAtHeightResponse, + > = querier.query_wasm_smart( + entry.addr, + &dao_interface::msg::QueryMsg::VotingPowerAtHeight { + address: hatcher.to_string(), + height: Some(entry.config.config_height), + }, + ); + + if let Ok(voting_power_response) = voting_power_response_result { + if voting_power_response.power > Uint128::zero() { + return Ok(entry.config.contribution_limits_override); + } + } + } + } + + Err(ContractError::SenderNotAllowlisted { + sender: hatcher.to_string(), + }) +} + +/// Set the maximum supply (only callable by owner) /// If `max_supply` is set to None there will be no limit.` pub fn update_max_supply( deps: DepsMut, @@ -337,40 +405,127 @@ pub fn update_max_supply( .add_attribute("value", max_supply.unwrap_or(Uint128::MAX).to_string())) } -/// Add and remove addresses from the hatcher allowlist +/// Toggles the paused state (only callable by owner) +pub fn toggle_pause(deps: DepsMut, info: MessageInfo) -> Result { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let is_paused = + IS_PAUSED.update(deps.storage, |is_paused| -> StdResult<_> { Ok(!is_paused) })?; + + Ok(Response::new() + .add_attribute("action", "toggle_pause") + .add_attribute("is_paused", is_paused.to_string())) +} + +/// Add and remove addresses from the hatcher allowlist (only callable by owner) pub fn update_hatch_allowlist( deps: DepsMut, + env: Env, info: MessageInfo, - to_add: Vec, + to_add: Vec, to_remove: Vec, ) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; - let mut allowlist = HATCHER_ALLOWLIST.may_load(deps.storage)?; - - if allowlist.is_none() { - allowlist = Some(HashSet::new()); - } - let allowlist = allowlist.as_mut().unwrap(); + let list = hatcher_allowlist(); // Add addresses to the allowlist for allow in to_add { - let addr = deps.api.addr_validate(allow.as_str())?; - allowlist.insert(addr); + let entry = allow.into_entry(deps.as_ref(), env.block.height)?; + + let old_data = list.may_load(deps.storage, &entry.addr)?; + + list.replace( + deps.storage, + &entry.addr, + Some(&entry.config), + old_data.as_ref(), + )?; + + // If the old data was previously a DAO config, then it should be removed + if let Some(old_data) = old_data { + try_remove_from_priority_queue(deps.storage, &entry.addr, &old_data)?; + } + + match allow.config.config_type { + HatcherAllowlistConfigType::DAO { priority } => { + if !HATCHER_DAO_PRIORITY_QUEUE.exists(deps.storage) { + HATCHER_DAO_PRIORITY_QUEUE.save(deps.storage, &vec![entry])?; + } else { + HATCHER_DAO_PRIORITY_QUEUE.update( + deps.storage, + |mut queue| -> StdResult<_> { + match priority { + Some(priority_value) => { + // Insert based on priority + let pos = queue + .binary_search_by(|entry| { + match &entry.config.config_type { + HatcherAllowlistConfigType::DAO { + priority: Some(entry_priority), + } => entry_priority + .cmp(&priority_value) + .then(std::cmp::Ordering::Less), + _ => std::cmp::Ordering::Less, // Treat non-DAO or DAO without priority as lower priority + } + }) + .unwrap_or_else(|e| e); + queue.insert(pos, entry); + } + None => { + // Append to the end if no priority + queue.push(entry); + } + } + + Ok(queue) + }, + )?; + } + } + HatcherAllowlistConfigType::Address {} => {} + } } // Remove addresses from the allowlist for deny in to_remove { let addr = deps.api.addr_validate(deny.as_str())?; - allowlist.remove(&addr); - } - HATCHER_ALLOWLIST.save(deps.storage, allowlist)?; + let old_data = list.may_load(deps.storage, &addr)?; + + if let Some(old_data) = old_data { + list.replace(deps.storage, &addr, None, Some(&old_data))?; + + try_remove_from_priority_queue(deps.storage, &addr, &old_data)?; + } + } Ok(Response::new().add_attributes(vec![("action", "update_hatch_allowlist")])) } -/// Update the configuration of a particular phase +fn try_remove_from_priority_queue( + storage: &mut dyn Storage, + addr: &Addr, + config: &HatcherAllowlistConfig, +) -> Result<(), ContractError> { + if matches!( + config.config_type, + HatcherAllowlistConfigType::DAO { priority: _ } + ) && HATCHER_DAO_PRIORITY_QUEUE.exists(storage) + { + HATCHER_DAO_PRIORITY_QUEUE.update(storage, |mut x| -> StdResult<_> { + if let Some(i) = x.iter().position(|y| y.addr == addr) { + x.remove(i); + } + + Ok(x) + })?; + } + + Ok(()) +} + +/// Update the configuration of a particular phase (only callable by owner) pub fn update_phase_config( deps: DepsMut, _env: Env, @@ -388,7 +543,6 @@ pub fn update_phase_config( match update_phase_config_msg { UpdatePhaseConfigMsg::Hatch { - exit_fee, initial_raise, entry_fee, contribution_limits, @@ -400,9 +554,6 @@ pub fn update_phase_config( if let Some(contribution_limits) = contribution_limits { phase_config.hatch.contribution_limits = contribution_limits; } - if let Some(exit_fee) = exit_fee { - phase_config.hatch.exit_fee = exit_fee; - } if let Some(initial_raise) = initial_raise { phase_config.hatch.initial_raise = initial_raise; } @@ -428,7 +579,7 @@ pub fn update_phase_config( phase_config.open.entry_fee = entry_fee; } if let Some(exit_fee) = exit_fee { - phase_config.hatch.exit_fee = exit_fee; + phase_config.open.exit_fee = exit_fee; } // Validate config @@ -442,7 +593,7 @@ pub fn update_phase_config( } } -/// Update the bonding curve. Only callable by the owner. +/// Update the bonding curve. (only callable by owner) /// NOTE: this changes the pricing. Use with caution. /// TODO: what other limitations do we want to put on this? pub fn update_curve( @@ -487,14 +638,14 @@ mod tests { mod donate { use super::*; use crate::abc::CurveType; - use crate::testing::mock_init; + use crate::testing::{mock_init, TEST_CREATOR}; use cosmwasm_std::coin; use cw_utils::PaymentError; const TEST_DONOR: &str = "donor"; fn exec_donate(deps: DepsMut, donation_amount: u128) -> Result { - execute_donate( + donate( deps, mock_env(), mock_info(TEST_DONOR, &[coin(donation_amount, TEST_RESERVE_DENOM)]), @@ -529,7 +680,7 @@ mod tests { let init_msg = default_instantiate_msg(2, 8, curve_type); mock_init(deps.as_mut(), init_msg)?; - let res = execute_donate( + let res = donate( deps.as_mut(), mock_env(), mock_info(TEST_DONOR, &[coin(1, "fake")]), @@ -544,22 +695,24 @@ mod tests { } #[test] - fn should_add_to_funding_pool() -> Result<(), ContractError> { + fn should_donate_to_forwarding() -> Result<(), ContractError> { let mut deps = mock_dependencies(); // this matches `linear_curve` test case from curves.rs let curve_type = CurveType::SquareRoot { slope: Uint128::new(1), scale: 1, }; - let init_msg = default_instantiate_msg(2, 8, curve_type); + let mut init_msg = default_instantiate_msg(2, 8, curve_type); + init_msg.funding_pool_forwarding = Some(TEST_CREATOR.to_string()); mock_init(deps.as_mut(), init_msg)?; let donation_amount = 5; let _res = exec_donate(deps.as_mut(), donation_amount)?; - // check that the curve's funding has been increased while supply and reserve have not + // Check that the funding pool did not increase, because it was sent to the funding pool forwarding + // NOTE: the balance cannot be checked with mock_dependencies let curve_state = CURVE_STATE.load(&deps.storage)?; - assert_that!(curve_state.funding).is_equal_to(Uint128::new(donation_amount)); + assert_that!(curve_state.funding).is_equal_to(Uint128::zero()); // check that the donor is in the donations map let donation = DONATIONS.load(&deps.storage, &Addr::unchecked(TEST_DONOR))?; @@ -567,5 +720,89 @@ mod tests { Ok(()) } + + #[test] + fn test_donate_and_withdraw() -> Result<(), ContractError> { + // Init + let mut deps = mock_dependencies(); + + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + // Donate + let donation_amount = 5; + let _res = exec_donate(deps.as_mut(), donation_amount)?; + + // Check funding pool + let curve_state = CURVE_STATE.load(&deps.storage)?; + assert_that!(curve_state.funding).is_equal_to(Uint128::from(donation_amount)); + + // Check random can't withdraw from the funding pool + let result = withdraw(deps.as_mut(), mock_env(), mock_info("random", &[]), None); + assert_that!(result) + .is_err() + .is_equal_to(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner, + )); + + // Check owner can withdraw + let result = withdraw( + deps.as_mut(), + mock_env(), + mock_info(crate::testing::TEST_CREATOR, &[]), + None, + ); + assert!(result.is_ok()); + + Ok(()) + } + + #[test] + fn test_pause() -> Result<(), ContractError> { + let mut deps = mock_dependencies(); + // this matches `linear_curve` test case from curves.rs + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + // Ensure not paused on instantiate + assert!(!IS_PAUSED.load(&deps.storage)?); + + // Ensure random cannot pause + let res = toggle_pause(deps.as_mut(), mock_info("random", &[])); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner, + )); + + // Ensure paused after toggling + toggle_pause(deps.as_mut(), mock_info(TEST_CREATOR, &[]))?; + assert!(IS_PAUSED.load(&deps.storage)?); + + // Ensure random cannot do anything + let res = crate::contract::execute( + deps.as_mut(), + mock_env(), + mock_info("random", &[]), + crate::msg::ExecuteMsg::TogglePause {}, + ); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Paused {}); + + // Ensure unpaused after toggling + toggle_pause(deps.as_mut(), mock_info(TEST_CREATOR, &[]))?; + assert!(!IS_PAUSED.load(&deps.storage)?); + + Ok(()) + } } } diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 02dbfd3b0..c14dfb723 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -1,23 +1,22 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, - Uint128, WasmMsg, + to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, + SubMsg, Uint128, WasmMsg, }; use cw2::set_contract_version; +use cw_curves::DecimalPlaces; use cw_tokenfactory_issuer::msg::{ DenomUnit, ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, Metadata, }; use cw_utils::parse_reply_instantiate_data; -use std::collections::HashSet; use crate::abc::{CommonsPhase, CurveFn}; -use crate::curves::DecimalPlaces; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::state::{ - CurveState, CURVE_STATE, CURVE_TYPE, FEES_RECIPIENT, HATCHER_ALLOWLIST, MAX_SUPPLY, PHASE, - PHASE_CONFIG, SUPPLY_DENOM, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, + CurveState, CURVE_STATE, CURVE_TYPE, FUNDING_POOL_FORWARDING, IS_PAUSED, MAX_SUPPLY, PHASE, + PHASE_CONFIG, SUPPLY_DENOM, TEMP_SUPPLY, TOKEN_ISSUER_CONTRACT, }; use crate::{commands, queries}; @@ -30,54 +29,45 @@ const INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID: u64 = 0; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let InstantiateMsg { - fees_recipient, + token_issuer_code_id, + funding_pool_forwarding, supply, reserve, curve_type, phase_config, hatcher_allowlist, - token_issuer_code_id, } = msg; + phase_config.validate()?; + + // Validate and store the funding pool forwarding + if let Some(funding_pool_forwarding) = funding_pool_forwarding { + FUNDING_POOL_FORWARDING.save( + deps.storage, + &deps.api.addr_validate(&funding_pool_forwarding)?, + )?; + } + if supply.subdenom.is_empty() { return Err(ContractError::SupplyTokenError( "Token subdenom must not be empty.".to_string(), )); } - phase_config.validate()?; - - // Validate and store the fees recipient - FEES_RECIPIENT.save(deps.storage, &deps.api.addr_validate(&fees_recipient)?)?; - - // Save new token info for use in reply - TOKEN_INSTANTIATION_INFO.save(deps.storage, &supply)?; - if let Some(max_supply) = supply.max_supply { MAX_SUPPLY.save(deps.storage, &max_supply)?; } - // Save the curve type and state - let normalization_places = DecimalPlaces::new(supply.decimals, reserve.decimals); - let curve_state = CurveState::new(reserve.denom, normalization_places); - CURVE_STATE.save(deps.storage, &curve_state)?; + // Save the curve type CURVE_TYPE.save(deps.storage, &curve_type)?; - if let Some(allowlist) = hatcher_allowlist { - let allowlist = allowlist - .into_iter() - .map(|addr| deps.api.addr_validate(addr.as_str())) - .collect::>>()?; - HATCHER_ALLOWLIST.save(deps.storage, &allowlist)?; - } - PHASE_CONFIG.save(deps.storage, &phase_config)?; // TODO don't hardcode this? Make it configurable? Hatch config can be optional @@ -86,14 +76,21 @@ pub fn instantiate( // Initialize owner to sender cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; - // Tnstantiate cw-token-factory-issuer contract - // Contract is immutable, no admin - let issuer_instantiate_msg = SubMsg::reply_always( + // Setup the curve state + let normalization_places = DecimalPlaces::new(supply.decimals, reserve.decimals); + let curve_state = CurveState::new(reserve.denom, normalization_places); + + // Save subdenom for handling in the reply + TEMP_SUPPLY.save(deps.storage, &supply)?; + + // Instantiate cw-token-factory-issuer contract + let msg = SubMsg::reply_always( WasmMsg::Instantiate { + // Contract is immutable, no admin admin: None, code_id: token_issuer_code_id, msg: to_json_binary(&IssuerInstantiateMsg::NewToken { - subdenom: supply.subdenom.clone(), + subdenom: supply.subdenom, })?, funds: info.funds, label: "cw-tokenfactory-issuer".to_string(), @@ -101,7 +98,27 @@ pub fn instantiate( INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, ); - Ok(Response::default().add_submessage(issuer_instantiate_msg)) + // Save the curve state + CURVE_STATE.save(deps.storage, &curve_state)?; + + // Set the paused state + IS_PAUSED.save(deps.storage, &false)?; + + // Set hatcher allowlist through internal method + let msgs = if let Some(hatcher_allowlist) = hatcher_allowlist { + vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::UpdateHatchAllowlist { + to_add: hatcher_allowlist, + to_remove: vec![], + })?, + funds: vec![], + })] + } else { + vec![] + }; + + Ok(Response::default().add_messages(msgs).add_submessage(msg)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -111,18 +128,29 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { + // If paused, then only the owner can perform actions + if IS_PAUSED.load(deps.storage)? { + cw_ownable::assert_owner(deps.storage, &info.sender) + .map_err(|_| ContractError::Paused {})?; + } + match msg { - ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info), - ExecuteMsg::Sell {} => commands::execute_sell(deps, env, info), - ExecuteMsg::Close {} => commands::execute_close(deps, info), - ExecuteMsg::Donate {} => commands::execute_donate(deps, env, info), + ExecuteMsg::Buy {} => commands::buy(deps, env, info), + ExecuteMsg::Sell {} => commands::sell(deps, env, info), + ExecuteMsg::Close {} => commands::close(deps, info), + ExecuteMsg::Donate {} => commands::donate(deps, env, info), + ExecuteMsg::Withdraw { amount } => commands::withdraw(deps, env, info, amount), + ExecuteMsg::UpdateFundingPoolForwarding { address } => { + commands::update_funding_pool_forwarding(deps, env, info, address) + } ExecuteMsg::UpdateMaxSupply { max_supply } => { commands::update_max_supply(deps, info, max_supply) } ExecuteMsg::UpdateCurve { curve_type } => commands::update_curve(deps, info, curve_type), ExecuteMsg::UpdateHatchAllowlist { to_add, to_remove } => { - commands::update_hatch_allowlist(deps, info, to_add, to_remove) + commands::update_hatch_allowlist(deps, env, info, to_add, to_remove) } + ExecuteMsg::TogglePause {} => commands::toggle_pause(deps, info), ExecuteMsg::UpdatePhaseConfig(update_msg) => { commands::update_phase_config(deps, env, info, update_msg) } @@ -153,15 +181,33 @@ pub fn do_query(deps: Deps, _env: Env, msg: QueryMsg, curve_fn: CurveFn) -> StdR QueryMsg::Donations { start_after, limit } => { to_json_binary(&queries::query_donations(deps, start_after, limit)?) } - QueryMsg::FeesRecipient {} => to_json_binary(&FEES_RECIPIENT.load(deps.storage)?), + QueryMsg::FundingPoolForwarding {} => { + to_json_binary(&FUNDING_POOL_FORWARDING.may_load(deps.storage)?) + } QueryMsg::Hatchers { start_after, limit } => { to_json_binary(&queries::query_hatchers(deps, start_after, limit)?) } + QueryMsg::Hatcher { addr } => to_json_binary(&queries::query_hatcher(deps, addr)?), + QueryMsg::HatcherAllowlist { + start_after, + limit, + config_type, + } => to_json_binary(&queries::query_hatcher_allowlist( + deps, + start_after, + limit, + config_type, + )?), + QueryMsg::IsPaused {} => to_json_binary(&IS_PAUSED.load(deps.storage)?), QueryMsg::MaxSupply {} => to_json_binary(&queries::query_max_supply(deps)?), QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), QueryMsg::PhaseConfig {} => to_json_binary(&queries::query_phase_config(deps)?), QueryMsg::Phase {} => to_json_binary(&PHASE.load(deps.storage)?), QueryMsg::TokenContract {} => to_json_binary(&TOKEN_ISSUER_CONTRACT.load(deps.storage)?), + QueryMsg::BuyQuote { payment } => to_json_binary(&queries::query_buy_quote(deps, payment)?), + QueryMsg::SellQuote { payment } => { + to_json_binary(&queries::query_sell_quote(deps, payment)?) + } } } @@ -180,13 +226,15 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result StdDecimal; - - /// Returns the total price paid up to purchase supply tokens (integral) - /// `F(x)` from the README - fn reserve(&self, supply: Uint128) -> Uint128; - - /// Inverse of reserve. Returns how many tokens would be issued - /// with a total paid amount of reserve. - /// `F^-1(x)` from the README - fn supply(&self, reserve: Uint128) -> Uint128; -} - -/// decimal returns an object = num * 10 ^ -scale -/// We use this function in contract.rs rather than call the crate constructor -/// itself, in case we want to swap out the implementation, we can do it only in this file. -pub fn decimal>(num: T, scale: u32) -> Decimal { - Decimal::from_i128_with_scale(num.into() as i128, scale) -} - -/// StdDecimal stores as a u128 with 18 decimal points of precision -fn decimal_to_std(x: Decimal) -> StdDecimal { - // this seems straight-forward (if inefficient), converting via string representation - // TODO: execute errors better? Result? - StdDecimal::from_str(&x.to_string()).unwrap() - - // // maybe a better approach doing math, not sure about rounding - // - // // try to preserve decimal points, max 9 - // let digits = min(x.scale(), 9); - // let multiplier = 10u128.pow(digits); - // - // // we multiply up before we round off to u128, - // // let StdDecimal do its best to keep these decimal places - // let nominator = (x * decimal(multiplier, 0)).to_u128().unwrap(); - // StdDecimal::from_ratio(nominator, multiplier) -} - -/// spot price is always a constant value -pub struct Constant { - pub value: Decimal, - pub normalize: DecimalPlaces, -} - -impl Constant { - pub fn new(value: Decimal, normalize: DecimalPlaces) -> Self { - Self { value, normalize } - } -} - -impl Curve for Constant { - // we need to normalize value with the reserve decimal places - // (eg 0.1 value would return 100_000 if reserve was uatom) - fn spot_price(&self, _supply: Uint128) -> StdDecimal { - // f(x) = self.value - decimal_to_std(self.value) - } - - /// Returns total number of reserve tokens needed to purchase a given number of supply tokens. - /// Note that both need to be normalized. - fn reserve(&self, supply: Uint128) -> Uint128 { - // f(x) = supply * self.value - let reserve = self.normalize.from_supply(supply) * self.value; - self.normalize.clone().to_reserve(reserve) - } - - fn supply(&self, reserve: Uint128) -> Uint128 { - // f(x) = reserve / self.value - let supply = self.normalize.from_reserve(reserve) / self.value; - self.normalize.clone().to_supply(supply) - } -} - -/// spot_price is slope * supply -pub struct Linear { - pub slope: Decimal, - pub normalize: DecimalPlaces, -} - -impl Linear { - pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { - Self { slope, normalize } - } -} - -impl Curve for Linear { - fn spot_price(&self, supply: Uint128) -> StdDecimal { - // f(x) = supply * self.value - let out = self.normalize.from_supply(supply) * self.slope; - decimal_to_std(out) - } - - fn reserve(&self, supply: Uint128) -> Uint128 { - // f(x) = self.slope * supply * supply / 2 - let normalized = self.normalize.from_supply(supply); - let square = normalized * normalized; - // Note: multiplying by 0.5 is much faster than dividing by 2 - let reserve = square * self.slope * Decimal::new(5, 1); - self.normalize.clone().to_reserve(reserve) - } - - fn supply(&self, reserve: Uint128) -> Uint128 { - // f(x) = (2 * reserve / self.slope) ^ 0.5 - // note: use addition here to optimize 2* operation - let square = self.normalize.from_reserve(reserve + reserve) / self.slope; - let supply = square_root(square); - self.normalize.clone().to_supply(supply) - } -} - -/// spot_price is slope * (supply)^0.5 -pub struct SquareRoot { - pub slope: Decimal, - pub normalize: DecimalPlaces, -} - -impl SquareRoot { - pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { - Self { slope, normalize } - } -} - -impl Curve for SquareRoot { - fn spot_price(&self, supply: Uint128) -> StdDecimal { - // f(x) = self.slope * supply^0.5 - let square = self.normalize.from_supply(supply); - let root = square_root(square); - decimal_to_std(root * self.slope) - } - - fn reserve(&self, supply: Uint128) -> Uint128 { - // f(x) = self.slope * supply * supply^0.5 / 1.5 - let normalized = self.normalize.from_supply(supply); - let root = square_root(normalized); - let reserve = self.slope * normalized * root / Decimal::new(15, 1); - self.normalize.clone().to_reserve(reserve) - } - - fn supply(&self, reserve: Uint128) -> Uint128 { - // f(x) = (1.5 * reserve / self.slope) ^ (2/3) - let base = self.normalize.from_reserve(reserve) * Decimal::new(15, 1) / self.slope; - let squared = base * base; - let supply = cube_root(squared); - self.normalize.clone().to_supply(supply) - } -} - -// we multiply by 10^18, turn to int, take square root, then divide by 10^9 as we convert back to decimal -fn square_root(square: Decimal) -> Decimal { - // must be even - // TODO: this can overflow easily at 18... what is a good value? - const EXTRA_DIGITS: u32 = 12; - let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); - - // multiply by 10^18 and turn to u128 - let extended = square * decimal(multiplier, 0); - let extended = extended.floor().to_u128().unwrap(); - - // take square root, and build a decimal again - let root = extended.integer_sqrt(); - decimal(root, EXTRA_DIGITS / 2) -} - -// we multiply by 10^9, turn to int, take cube root, then divide by 10^3 as we convert back to decimal -fn cube_root(cube: Decimal) -> Decimal { - // must be multiple of 3 - // TODO: what is a good value? - const EXTRA_DIGITS: u32 = 9; - let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); - - // multiply out and turn to u128 - let extended = cube * decimal(multiplier, 0); - let extended = extended.floor().to_u128().unwrap(); - - // take cube root, and build a decimal again - let root = extended.integer_cbrt(); - decimal(root, EXTRA_DIGITS / 3) -} - -/// DecimalPlaces should be passed into curve constructors -#[cw_serde] -pub struct DecimalPlaces { - /// Number of decimal places for the supply token (this is what was passed in cw20-base instantiate - pub supply: u32, - /// Number of decimal places for the reserve token (eg. 6 for uatom, 9 for nstep, 18 for wei) - pub reserve: u32, -} - -impl DecimalPlaces { - pub fn new(supply: u8, reserve: u8) -> Self { - DecimalPlaces { - supply: supply as u32, - reserve: reserve as u32, - } - } - - pub fn to_reserve(self, reserve: Decimal) -> Uint128 { - let factor = decimal(10u128.pow(self.reserve), 0); - let out = reserve * factor; - // TODO: execute overflow better? Result? - out.floor().to_u128().unwrap().into() - } - - pub fn to_supply(self, supply: Decimal) -> Uint128 { - let factor = decimal(10u128.pow(self.supply), 0); - let out = supply * factor; - // TODO: execute overflow better? Result? - out.floor().to_u128().unwrap().into() - } - - pub fn from_supply(&self, supply: Uint128) -> Decimal { - decimal(supply, self.supply) - } - - pub fn from_reserve(&self, reserve: Uint128) -> Decimal { - decimal(reserve, self.reserve) - } -} - -#[cfg(test)] -mod tests { - use super::*; - // TODO: test DecimalPlaces return proper decimals - - #[test] - fn constant_curve() { - // supply is nstep (9), reserve is uatom (6) - let normalize = DecimalPlaces::new(9, 6); - let curve = Constant::new(decimal(15u128, 1), normalize); - - // do some sanity checks.... - // spot price is always 1.5 ATOM - assert_eq!( - StdDecimal::percent(150), - curve.spot_price(Uint128::new(123)) - ); - - // if we have 30 STEP, we should have 45 ATOM - let reserve = curve.reserve(Uint128::new(30_000_000_000)); - assert_eq!(Uint128::new(45_000_000), reserve); - - // if we have 36 ATOM, we should have 24 STEP - let supply = curve.supply(Uint128::new(36_000_000)); - assert_eq!(Uint128::new(24_000_000_000), supply); - } - - #[test] - fn linear_curve() { - // supply is usdt (2), reserve is btc (8) - let normalize = DecimalPlaces::new(2, 8); - // slope is 0.1 (eg hits 1.0 after 10btc) - let curve = Linear::new(decimal(1u128, 1), normalize); - - // do some sanity checks.... - // spot price is 0.1 with 1 USDT supply - assert_eq!( - StdDecimal::permille(100), - curve.spot_price(Uint128::new(100)) - ); - // spot price is 1.7 with 17 USDT supply - assert_eq!( - StdDecimal::permille(1700), - curve.spot_price(Uint128::new(1700)) - ); - // spot price is 0.212 with 2.12 USDT supply - assert_eq!( - StdDecimal::permille(212), - curve.spot_price(Uint128::new(212)) - ); - - // if we have 10 USDT, we should have 5 BTC - let reserve = curve.reserve(Uint128::new(1000)); - assert_eq!(Uint128::new(500_000_000), reserve); - // if we have 20 USDT, we should have 20 BTC - let reserve = curve.reserve(Uint128::new(2000)); - assert_eq!(Uint128::new(2_000_000_000), reserve); - - // if we have 1.25 BTC, we should have 5 USDT - let supply = curve.supply(Uint128::new(125_000_000)); - assert_eq!(Uint128::new(500), supply); - // test square root rounding - // TODO: test when supply has many more decimal places than reserve - // if we have 1.11 BTC, we should have 4.7116875957... USDT - let supply = curve.supply(Uint128::new(111_000_000)); - assert_eq!(Uint128::new(471), supply); - } - - #[test] - fn sqrt_curve() { - // supply is utree (6) reserve is chf (2) - let normalize = DecimalPlaces::new(6, 2); - // slope is 0.35 (eg hits 0.35 after 1 chf, 3.5 after 100chf) - let curve = SquareRoot::new(decimal(35u128, 2), normalize); - - // do some sanity checks.... - // spot price is 0.35 with 1 TREE supply - assert_eq!( - StdDecimal::percent(35), - curve.spot_price(Uint128::new(1_000_000)) - ); - // spot price is 3.5 with 100 TREE supply - assert_eq!( - StdDecimal::percent(350), - curve.spot_price(Uint128::new(100_000_000)) - ); - // spot price should be 23.478713763747788 with 4500 TREE supply (test rounding and reporting here) - // rounds off around 8-9 sig figs (note diff for last points) - assert_eq!( - StdDecimal::from_ratio(2347871365u128, 100_000_000u128), - curve.spot_price(Uint128::new(4_500_000_000)) - ); - - // if we have 1 TREE, we should have 0.2333333333333 CHF - let reserve = curve.reserve(Uint128::new(1_000_000)); - assert_eq!(Uint128::new(23), reserve); - // if we have 100 TREE, we should have 233.333333333 CHF - let reserve = curve.reserve(Uint128::new(100_000_000)); - assert_eq!(Uint128::new(23_333), reserve); - // test rounding - // if we have 235 TREE, we should have 840.5790828021146 CHF - let reserve = curve.reserve(Uint128::new(235_000_000)); - assert_eq!(Uint128::new(84_057), reserve); // round down - - // // if we have 0.23 CHF, we should have 0.990453 TREE (round down) - let supply = curve.supply(Uint128::new(23)); - assert_eq!(Uint128::new(990_000), supply); - // if we have 840.58 CHF, we should have 235.000170 TREE (round down) - let supply = curve.supply(Uint128::new(84058)); - assert_eq!(Uint128::new(235_000_000), supply); - } - - // Idea: generic test that curve.supply(curve.reserve(supply)) == supply (or within some small rounding margin) -} diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index 6c343f21b..ef59faafc 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{StdError, Uint128}; +use cosmwasm_std::{CheckedMultiplyFractionError, OverflowError, StdError, Uint128}; use cw_utils::{ParseReplyError, PaymentError}; use thiserror::Error; @@ -7,6 +7,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + Overflow(#[from] OverflowError), + #[error(transparent)] Payment(#[from] PaymentError), @@ -16,12 +19,18 @@ pub enum ContractError { #[error("{0}")] Ownership(#[from] cw_ownable::OwnershipError), + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + #[error("Cannot mint more tokens than the maximum supply of {max}")] CannotExceedMaxSupply { max: Uint128 }, #[error("The commons is closed to new contributions")] CommonsClosed {}, + #[error("The commons is locked against liquidations")] + CommonsHatch {}, + #[error("Contribution must be less than or equal to {max} and greater than or equal to {min}")] ContributionLimit { min: Uint128, max: Uint128 }, @@ -54,4 +63,7 @@ pub enum ContractError { #[error("Got a submessage reply with unknown id: {id}")] UnknownReplyId { id: u64 }, + + #[error("Contract is paused")] + Paused {}, } diff --git a/contracts/external/cw-abc/src/helpers.rs b/contracts/external/cw-abc/src/helpers.rs new file mode 100644 index 000000000..96a275ec9 --- /dev/null +++ b/contracts/external/cw-abc/src/helpers.rs @@ -0,0 +1,109 @@ +use cosmwasm_std::{Decimal, Deps, StdResult, Uint128}; + +use crate::{ + abc::{CommonsPhase, CommonsPhaseConfig, CurveType}, + msg::{HatcherAllowlistEntryMsg, QuoteResponse}, + state::{CurveState, HatcherAllowlistConfig, HatcherAllowlistEntry}, + ContractError, +}; + +/// Calculate the buy quote for a payment +pub fn calculate_buy_quote( + payment: Uint128, + curve_type: &CurveType, + curve_state: &CurveState, + phase: &CommonsPhase, + phase_config: &CommonsPhaseConfig, +) -> Result { + // Generate the bonding curve + let curve_fn = curve_type.to_curve_fn(); + let curve = curve_fn(curve_state.decimals); + + // Calculate the reserved and funded amounts based on the Commons phase + let (reserved, funded) = match phase { + CommonsPhase::Hatch => calculate_reserved_and_funded(payment, phase_config.hatch.entry_fee), + CommonsPhase::Open => calculate_reserved_and_funded(payment, phase_config.open.entry_fee), + CommonsPhase::Closed => Err(ContractError::CommonsClosed {}), + }?; + + // Update the reserve and calculate the new supply from the new reserve + let new_reserve = curve_state.reserve.checked_add(reserved)?; + let new_supply = curve.supply(new_reserve); + + // Calculate the difference between the new and old supply to get the minted tokens + let minted = new_supply.checked_sub(curve_state.supply)?; + + Ok(QuoteResponse { + new_reserve, + funded, + amount: minted, + new_supply, + }) +} + +/// Calculate the sell quote for a payment +pub fn calculate_sell_quote( + payment: Uint128, + curve_type: &CurveType, + curve_state: &CurveState, + phase: &CommonsPhase, + phase_config: &CommonsPhaseConfig, +) -> Result { + // Generate the bonding curve + let curve_fn = curve_type.to_curve_fn(); + let curve = curve_fn(curve_state.decimals); + + // Reduce the supply by the amount being burned + let new_supply = curve_state.supply.checked_sub(payment)?; + + // Determine the exit fee based on the current Commons phase + let exit_fee = match &phase { + CommonsPhase::Hatch => Err(ContractError::CommonsHatch {}), + CommonsPhase::Open => Ok(phase_config.open.exit_fee), + CommonsPhase::Closed => Ok(Decimal::zero()), + }?; + + // Calculate the new reserve based on the new supply + let new_reserve = curve.reserve(new_supply); + + // Calculate how many reserve tokens to release based on the amount being burned + let released_reserve = curve_state.reserve.checked_sub(new_reserve)?; + + // Calculate the reserved and funded amounts based on the exit fee + let (reserved, funded) = calculate_reserved_and_funded(released_reserve, exit_fee)?; + + Ok(QuoteResponse { + new_reserve, + funded, + amount: reserved, + new_supply, + }) +} + +/// Return the reserved and funded amounts based on the payment and the allocation ratio +pub(crate) fn calculate_reserved_and_funded( + payment: Uint128, + allocation_ratio: Decimal, +) -> Result<(Uint128, Uint128), ContractError> { + if allocation_ratio.is_zero() { + return Ok((payment, Uint128::zero())); + } + + let funded = payment.checked_mul_floor(allocation_ratio)?; + let reserved = payment - funded; // Since allocation_ratio is < 1, this subtraction is safe + + Ok((reserved, funded)) +} + +impl HatcherAllowlistEntryMsg { + pub fn into_entry(&self, deps: Deps, height: u64) -> StdResult { + Ok(HatcherAllowlistEntry { + addr: deps.api.addr_validate(&self.addr)?, + config: HatcherAllowlistConfig { + config_type: self.config.config_type, + contribution_limits_override: self.config.contribution_limits_override, + config_height: height, + }, + }) + } +} diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index d8a18c1cb..275d47efd 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -1,13 +1,13 @@ pub mod abc; pub(crate) mod commands; pub mod contract; -pub mod curves; mod error; +pub(crate) mod helpers; pub mod msg; mod queries; pub mod state; -// Integrationg tests using an actual chain binary, requires +// Integration tests using an actual chain binary, requires // the "test-tube" feature to be enabled // cargo test --features test-tube #[cfg(test)] diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index c201760e2..f8ee0d289 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -1,16 +1,19 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Decimal as StdDecimal, Uint128}; +use cosmwasm_std::{Addr, Decimal, Uint128}; -use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}; +use crate::{ + abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}, + state::{HatcherAllowlistConfigType, HatcherAllowlistEntry}, +}; #[cw_serde] pub struct InstantiateMsg { - /// The recipient for any fees collected from bonding curve operation - pub fees_recipient: String, - /// The code id of the cw-tokenfactory-issuer contract pub token_issuer_code_id: u64, + /// An optional address for automatically forwarding funding pool gains + pub funding_pool_forwarding: Option, + /// Supply token information pub supply: SupplyToken, @@ -26,7 +29,7 @@ pub struct InstantiateMsg { /// TODO different ways of doing this, for example DAO members? /// Using a whitelist contract? Merkle tree? /// Hatcher allowlist - pub hatcher_allowlist: Option>, + pub hatcher_allowlist: Option>, } /// Update the phase configurations. @@ -38,13 +41,12 @@ pub enum UpdatePhaseConfigMsg { contribution_limits: Option, // TODO what is the minimum used for? initial_raise: Option, - entry_fee: Option, - exit_fee: Option, + entry_fee: Option, }, /// Update the open phase configuration. Open { - exit_fee: Option, - entry_fee: Option, + exit_fee: Option, + entry_fee: Option, }, /// Update the closed phase configuration. /// TODO Set the curve type to be used on close? @@ -60,8 +62,14 @@ pub enum ExecuteMsg { /// Sell burns supply tokens in return for the reserve token. /// You must send only supply tokens. Sell {}, - /// Donate will add reserve tokens to the funding pool + /// Donate will donate tokens to the funding pool. + /// You must send only reserve tokens. Donate {}, + /// Withdraw will withdraw tokens from the funding pool. + Withdraw { + /// The amount to withdraw (defaults to full amount). + amount: Option, + }, /// Sets (or unsets if set to None) the maximum supply UpdateMaxSupply { /// The maximum supply able to be minted. @@ -72,13 +80,22 @@ pub enum ExecuteMsg { /// TODO think about other potential limitations on this. UpdateCurve { curve_type: CurveType }, /// Update the hatch phase allowlist. - /// This can only be called by the owner. + /// Only callable by owner. UpdateHatchAllowlist { /// Addresses to be added. - to_add: Vec, + to_add: Vec, /// Addresses to be removed. to_remove: Vec, }, + /// Toggles the paused state (circuit breaker) + TogglePause {}, + /// Update the funding pool forwarding. + /// Only callable by owner. + UpdateFundingPoolForwarding { + /// The address to receive the funding pool forwarding. + /// Set to None to stop forwarding. + address: Option, + }, /// Update the configuration of a certain phase. /// This can only be called by the owner. UpdatePhaseConfig(UpdatePhaseConfigMsg), @@ -109,10 +126,12 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - /// Returns the Fee Recipient for the contract. This is the address that - /// recieves any fees collected from bonding curve operation - #[returns(::cosmwasm_std::Addr)] - FeesRecipient {}, + #[returns(bool)] + IsPaused {}, + /// Returns the funding pool forwarding config for the contract. This is the address that + /// receives any fees collected from bonding curve operation and donations + #[returns(Option<::cosmwasm_std::Addr>)] + FundingPoolForwarding {}, /// List the hatchers and their contributions /// Returns [`HatchersResponse`] #[returns(HatchersResponse)] @@ -120,9 +139,26 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - /// Returns the Maxiumum Supply of the supply token + /// Returns the contribution of a hatcher + #[returns(Uint128)] + Hatcher { addr: String }, + /// Lists the hatcher allowlist + /// Returns [`HatcherAllowlistResponse`] + #[returns(HatcherAllowlistResponse)] + HatcherAllowlist { + start_after: Option, + limit: Option, + config_type: Option, + }, + /// Returns the Maximum Supply of the supply token #[returns(Uint128)] MaxSupply {}, + /// Returns the amount of tokens to receive from buying + #[returns(QuoteResponse)] + BuyQuote { payment: Uint128 }, + /// Returns the amount of tokens to receive from selling + #[returns(QuoteResponse)] + SellQuote { payment: Uint128 }, /// Returns the current phase #[returns(CommonsPhase)] Phase {}, @@ -135,6 +171,20 @@ pub enum QueryMsg { TokenContract {}, } +#[cw_serde] +pub struct HatcherAllowlistEntryMsg { + pub addr: String, + pub config: HatcherAllowlistConfigMsg, +} + +#[cw_serde] +pub struct HatcherAllowlistConfigMsg { + /// The type of the configuration + pub config_type: HatcherAllowlistConfigType, + /// An optional override of the hatch_config's contribution limit + pub contribution_limits_override: Option, +} + #[cw_serde] pub struct CurveInfoResponse { /// How many reserve tokens have been received @@ -157,7 +207,7 @@ pub struct DenomResponse { #[cw_serde] pub struct HatcherAllowlistResponse { /// Hatcher allowlist - pub allowlist: Option>, + pub allowlist: Option>, } #[cw_serde] @@ -181,5 +231,13 @@ pub struct HatchersResponse { pub hatchers: Vec<(Addr, Uint128)>, } +#[cw_serde] +pub struct QuoteResponse { + pub new_reserve: Uint128, + pub funded: Uint128, + pub amount: Uint128, + pub new_supply: Uint128, +} + #[cw_serde] pub struct MigrateMsg {} diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index 8bc9138a6..0e07a8e28 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -1,12 +1,15 @@ use crate::abc::CurveFn; +use crate::helpers::{calculate_buy_quote, calculate_sell_quote}; use crate::msg::{ CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, DonationsResponse, - HatchersResponse, + HatcherAllowlistResponse, HatchersResponse, QuoteResponse, }; use crate::state::{ - CurveState, CURVE_STATE, DONATIONS, HATCHERS, MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, + hatcher_allowlist, CurveState, HatcherAllowlistConfigType, HatcherAllowlistEntry, CURVE_STATE, + CURVE_TYPE, DONATIONS, HATCHERS, MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, }; -use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult, Uint128}; +use cosmwasm_std::{Deps, Order, QuerierWrapper, StdError, StdResult, Uint128}; +use cw_storage_plus::Bound; use std::ops::Deref; /// Get the current state of the curve @@ -85,6 +88,51 @@ pub fn query_hatchers( Ok(HatchersResponse { hatchers }) } +/// Query the contribution of a hatcher +pub fn query_hatcher(deps: Deps, addr: String) -> StdResult { + let addr = deps.api.addr_validate(&addr)?; + + HATCHERS.load(deps.storage, &addr) +} + +/// Query hatcher allowlist +pub fn query_hatcher_allowlist( + deps: Deps, + start_after: Option, + limit: Option, + config_type: Option, +) -> StdResult { + if hatcher_allowlist().is_empty(deps.storage) { + return Ok(HatcherAllowlistResponse { allowlist: None }); + } + + let binding = start_after + .map(|x| deps.api.addr_validate(&x)) + .transpose()?; + let start_after_bound = binding.as_ref().map(Bound::exclusive); + + let iter = match config_type { + Some(config_type) => hatcher_allowlist() + .idx + .config_type + .prefix(config_type.to_string()) + .range(deps.storage, start_after_bound, None, Order::Ascending), + None => hatcher_allowlist().range(deps.storage, start_after_bound, None, Order::Ascending), + } + .map(|result| result.map(|(addr, config)| HatcherAllowlistEntry { addr, config })); + + let allowlist = match limit { + Some(limit) => iter + .take(limit.try_into().unwrap()) + .collect::>(), + None => iter.collect::>(), + }?; + + Ok(HatcherAllowlistResponse { + allowlist: Some(allowlist), + }) +} + /// Query the max supply of the supply token pub fn query_max_supply(deps: Deps) -> StdResult { let max_supply = MAX_SUPPLY.may_load(deps.storage)?; @@ -100,3 +148,25 @@ pub fn query_phase_config(deps: Deps) -> StdResult { phase, }) } + +/// Get a buy quote +pub fn query_buy_quote(deps: Deps, payment: Uint128) -> StdResult { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let curve_state = CURVE_STATE.load(deps.storage)?; + let phase_config = PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + + calculate_buy_quote(payment, &curve_type, &curve_state, &phase, &phase_config) + .map_err(|e| StdError::generic_err(e.to_string())) +} + +/// Get a sell quote +pub fn query_sell_quote(deps: Deps, payment: Uint128) -> StdResult { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let curve_state = CURVE_STATE.load(deps.storage)?; + let phase_config = PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + + calculate_sell_quote(payment, &curve_type, &curve_state, &phase, &phase_config) + .map_err(|e| StdError::generic_err(e.to_string())) +} diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs index dc4f6b022..74393cf0a 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -1,11 +1,10 @@ -use cosmwasm_schema::cw_serde; -use std::collections::HashSet; - -use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, SupplyToken}; -use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::{Item, Map}; +use std::fmt::{self, Display}; -use crate::curves::DecimalPlaces; +use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, SupplyToken}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128, Uint64}; +use cw_curves::DecimalPlaces; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; /// Supply is dynamic and tracks the current supply of staked and ERC20 tokens. #[cw_serde] @@ -17,10 +16,10 @@ pub struct CurveState { /// supply is how many tokens this contract has issued pub supply: Uint128, - // the denom of the reserve token + /// the denom of the reserve token pub reserve_denom: String, - // how to normalize reserve and supply + /// how to normalize reserve and supply pub decimals: DecimalPlaces, } @@ -36,12 +35,88 @@ impl CurveState { } } +/// The configuration for a member of the hatcher allowlist +#[cw_serde] +pub struct HatcherAllowlistConfig { + /// The type of the configuration + pub config_type: HatcherAllowlistConfigType, + /// An optional override of the hatch_config's contribution limit + pub contribution_limits_override: Option, + /// The height of the config insertion + /// For use when checking allowlist of DAO configs + pub config_height: u64, +} + +#[cw_serde] +pub struct HatcherAllowlistEntry { + pub addr: Addr, + pub config: HatcherAllowlistConfig, +} + +#[cw_serde] +pub enum HatcherAllowlistConfigType { + DAO { + /// The optional priority for checking a DAO config + /// None will append the item to the end of the priority queue (least priority) + priority: Option, + }, + Address {}, +} + +impl Copy for HatcherAllowlistConfigType {} + +impl Display for HatcherAllowlistConfigType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HatcherAllowlistConfigType::DAO { priority: _ } => write!(f, "DAO"), + HatcherAllowlistConfigType::Address {} => write!(f, "Address"), + } + } +} + +pub struct HatcherAllowlistIndexes<'a> { + pub config_type: MultiIndex<'a, String, HatcherAllowlistConfig, &'a Addr>, +} + +impl<'a> IndexList for HatcherAllowlistIndexes<'a> { + fn get_indexes( + &'_ self, + ) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.config_type]; + Box::new(v.into_iter()) + } +} + +pub fn hatcher_allowlist<'a>( +) -> IndexedMap<'a, &'a Addr, HatcherAllowlistConfig, HatcherAllowlistIndexes<'a>> { + let indexes = HatcherAllowlistIndexes { + config_type: MultiIndex::new( + |_, x: &HatcherAllowlistConfig| x.config_type.to_string(), + "hatcher_allowlist", + "hatcher_allowlist__config_type", + ), + }; + + IndexedMap::new("hatcher_allowlist", indexes) +} + +/// The hatcher allowlist with configurations +pub const HATCHER_ALLOWLIST: Map<&Addr, HatcherAllowlistConfig> = Map::new("hatcher_allowlist"); + +/// The DAO portion of the hatcher allowlist implemented as a priority queue +/// If someone is a member of multiple allowlisted DAO's, we want to be able to control the checking order +pub const HATCHER_DAO_PRIORITY_QUEUE: Item> = + Item::new("HATCHER_DAO_PRIORITY_QUEUE"); + +/// The paused state for implementing a circuit breaker +pub const IS_PAUSED: Item = Item::new("is_paused"); + pub const CURVE_STATE: Item = Item::new("curve_state"); pub const CURVE_TYPE: Item = Item::new("curve_type"); -/// The recipient for fees generated from bonding curve operation -pub const FEES_RECIPIENT: Item = Item::new("fees_recipient"); +/// The address for automatically forwarding funding pool gains +pub const FUNDING_POOL_FORWARDING: Item = Item::new("funding_pool_forwarding"); /// The denom used for the supply token pub const SUPPLY_DENOM: Item = Item::new("denom"); @@ -49,10 +124,6 @@ pub const SUPPLY_DENOM: Item = Item::new("denom"); /// The maximum supply of the supply token, new tokens cannot be minted beyond this cap pub const MAX_SUPPLY: Item = Item::new("max_supply"); -/// Hatcher phase allowlist -/// TODO: we could use the keys for the [`HATCHERS`] map instead setting them to 0 at the beginning, though existing hatchers would not be able to be removed -pub static HATCHER_ALLOWLIST: Item> = Item::new("hatch_allowlist"); - /// Keep track of who has contributed to the hatch phase /// TODO: cw-set? This should be a map because in the open-phase we need to be able /// to ascertain the amount contributed by a user @@ -67,8 +138,8 @@ pub static PHASE_CONFIG: Item = Item::new("phase_config"); /// The phase state of the Augmented Bonding Curve pub static PHASE: Item = Item::new("phase"); -/// Temporarily holds token_instantiation_info when creating a new Token Factory denom -pub const TOKEN_INSTANTIATION_INFO: Item = Item::new("token_instantiation_info"); +/// Temporarily holds the supply config when creating a new Token Factory denom +pub const TEMP_SUPPLY: Item = Item::new("temp_supply"); /// The address of the cw-tokenfactory-issuer contract pub const TOKEN_ISSUER_CONTRACT: Item = Item::new("token_issuer_contract"); diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 7f88f5a5d..0f0681d80 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -4,15 +4,17 @@ use crate::{ ReserveToken, SupplyToken, }, msg::{ - CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, InstantiateMsg, - QueryMsg, + CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, + HatcherAllowlistConfigMsg, HatcherAllowlistEntryMsg, InstantiateMsg, QueryMsg, + QuoteResponse, }, + state::HatcherAllowlistConfigType, ContractError, }; use super::test_env::{TestEnv, TestEnvBuilder, DENOM, RESERVE}; -use cosmwasm_std::{coins, Decimal, Uint128}; +use cosmwasm_std::{coins, Decimal, Uint128, Uint64}; use cw_tokenfactory_issuer::msg::QueryMsg as IssuerQueryMsg; use osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest; use osmosis_test_tube::{osmosis_std::types::cosmos::base::v1beta1::Coin, Account, OsmosisTestApp}; @@ -29,6 +31,22 @@ fn test_happy_path() { .. } = env; + // Query buy quote + let quote = abc + .query::(&QueryMsg::BuyQuote { + payment: Uint128::new(1000u128), + }) + .unwrap(); + assert_eq!( + quote, + QuoteResponse { + new_reserve: Uint128::new(900u128), + funded: Uint128::new(100u128), + amount: Uint128::new(9000u128), + new_supply: Uint128::new(9000u128), + } + ); + // Buy tokens abc.execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[0]) .unwrap(); @@ -78,7 +96,7 @@ fn test_happy_path() { CurveInfoResponse { reserve: Uint128::new(900), supply: Uint128::new(9000), - funding: Uint128::new(100), + funding: Uint128::new(0), spot_price: Decimal::percent(10u64), reserve_denom: RESERVE.to_string(), } @@ -86,7 +104,7 @@ fn test_happy_path() { // Query phase let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); - assert_eq!(phase.phase, CommonsPhase::Hatch); + assert!(matches!(phase.phase, CommonsPhase::Hatch)); assert_eq!( phase.phase_config, CommonsPhaseConfig { @@ -97,10 +115,9 @@ fn test_happy_path() { }, initial_raise: MinMax { min: Uint128::from(10u128), - max: Uint128::from(1000000u128), + max: Uint128::from(900_000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -110,10 +127,44 @@ fn test_happy_path() { } ); + // Trying to sell is an error + let err = abc + .execute( + &ExecuteMsg::Sell {}, + &coins(1000, denom.clone()), + &accounts[0], + ) + .unwrap_err(); + assert_eq!(err, abc.execute_error(ContractError::CommonsHatch {})); + + // Buy enough tokens to end the hatch phase + abc.execute(&ExecuteMsg::Buy {}, &coins(999999, RESERVE), &accounts[1]) + .unwrap(); + + // Contract is now in open phase + let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); + assert_eq!(phase.phase, CommonsPhase::Open); + + // Query sell quote + let quote = abc + .query::(&QueryMsg::SellQuote { + payment: Uint128::new(1000u128), + }) + .unwrap(); + assert_eq!( + quote, + QuoteResponse { + new_reserve: Uint128::new(900800u128), + funded: Uint128::new(10u128), + amount: Uint128::new(90u128), + new_supply: Uint128::new(9008000u128), + } + ); + // Sell abc.execute( &ExecuteMsg::Sell {}, - &coins(100, denom.clone()), + &coins(1000, denom.clone()), &accounts[0], ) .unwrap(); @@ -123,9 +174,9 @@ fn test_happy_path() { assert_eq!( curve_info, CurveInfoResponse { - reserve: Uint128::new(890), - supply: Uint128::new(8900), - funding: Uint128::new(101), + reserve: Uint128::new(900800u128), + supply: Uint128::new(9008000u128), + funding: Uint128::new(0), spot_price: Decimal::percent(10u64), reserve_denom: RESERVE.to_string(), } @@ -152,24 +203,16 @@ fn test_happy_path() { user_balance.balance, Some(Coin { denom: denom.clone(), - amount: "8900".to_string(), + amount: "8000".to_string(), }) ); assert_eq!( contract_balance.balance, Some(Coin { denom: RESERVE.to_string(), - amount: "890".to_string(), + amount: "900800".to_string(), }) ); - - // Buy enough tokens to end the hatch phase - abc.execute(&ExecuteMsg::Buy {}, &coins(999999, RESERVE), &accounts[1]) - .unwrap(); - - // Contract is now in open phase - let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); - assert_eq!(phase.phase, CommonsPhase::Open); } #[test] @@ -187,7 +230,7 @@ fn test_contribution_limits_enforced() { let err = abc .execute( &ExecuteMsg::Buy {}, - &coins(1000000000, RESERVE), + &coins(1_000_000_000, RESERVE), &accounts[0], ) .unwrap_err(); @@ -225,8 +268,12 @@ fn test_max_supply() { } = env; // Buy enough tokens to end the hatch phase - abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, RESERVE), &accounts[0]) - .unwrap(); + abc.execute( + &ExecuteMsg::Buy {}, + &coins(1_000_000, RESERVE), + &accounts[0], + ) + .unwrap(); // Buy enough tokens to trigger a max supply error let err = abc @@ -280,8 +327,8 @@ fn test_allowlist() { let app = OsmosisTestApp::new(); let builder = TestEnvBuilder::new(); let instantiate_msg = InstantiateMsg { - fees_recipient: "replaced to accounts[0]".to_string(), token_issuer_code_id: 0, + funding_pool_forwarding: Some("replaced to accounts[0]".to_string()), supply: SupplyToken { subdenom: DENOM.to_string(), metadata: None, @@ -303,7 +350,6 @@ fn test_allowlist() { max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -328,7 +374,22 @@ fn test_allowlist() { let err = abc .execute( &ExecuteMsg::UpdateHatchAllowlist { - to_add: vec![accounts[0].address(), accounts[1].address()], + to_add: vec![ + HatcherAllowlistEntryMsg { + addr: accounts[0].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + HatcherAllowlistEntryMsg { + addr: accounts[1].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + ], to_remove: vec![], }, &[], @@ -346,7 +407,22 @@ fn test_allowlist() { // instantiation. abc.execute( &ExecuteMsg::UpdateHatchAllowlist { - to_add: vec![accounts[0].address(), accounts[1].address()], + to_add: vec![ + HatcherAllowlistEntryMsg { + addr: accounts[0].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + HatcherAllowlistEntryMsg { + addr: accounts[1].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + ], to_remove: vec![], }, &[], @@ -439,8 +515,12 @@ fn test_update_curve() { .denom; // Buy enough tokens to end the hatch phase - abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, RESERVE), &accounts[0]) - .unwrap(); + abc.execute( + &ExecuteMsg::Buy {}, + &coins(1_000_000, RESERVE), + &accounts[0], + ) + .unwrap(); // Only owner can update the curve let err = abc @@ -514,3 +594,181 @@ fn test_update_curve() { }) ); } + +#[test] +fn test_dao_hatcher() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + .. + } = env; + + // Setup a dao with the 1st half of accounts + let dao_ids = env.init_dao_ids(); + let daos: Vec<_> = (0..5) + .into_iter() + .map(|_| env.setup_default_dao(dao_ids)) + .collect(); + app.increase_time(1u64); + + // Update hatcher allowlist for DAO membership + // The max contribution of 50 should have the highest priority + for (i, dao) in daos.iter().enumerate() { + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: dao.contract_addr.to_string(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::DAO { + priority: Some(Uint64::MAX - Uint64::new(i as u64)), // Insert in reverse priority to ensure insertion ordering is valid + }, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(10u128) * Uint128::from(i as u128 + 1u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + } + + // Let's also insert a dao with no priority to make sure it's added to the end + let dao = env.setup_default_dao(dao_ids); + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: dao.contract_addr.to_string(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::DAO { priority: None }, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(100u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // Also add a DAO tied for the highest priority + // This should not update contribution limit, because the 1st DAO was added first and user is a member of it + let dao = env.setup_default_dao(dao_ids); + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: dao.contract_addr.to_string(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::DAO { + priority: Some(Uint64::MAX - Uint64::from(4u64)), + }, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(1000u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // Check contribution limit at this point + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[0]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::one(), + max: Uint128::from(50u128) + }) + ); + + // Check removing a dao config updates the contribution limit + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![], + to_remove: vec![daos.last().unwrap().contract_addr.to_string()], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // The error should say 1k is the max contribution now + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(2000, RESERVE), &accounts[0]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::one(), + max: Uint128::from(1000u128) + }) + ); + + // Adhering to the limit makes this ok now + let result = abc.execute(&ExecuteMsg::Buy {}, &coins(40, RESERVE), &accounts[0]); + assert!(result.is_ok()); + + // Check not allowlisted + let result = abc.execute( + &ExecuteMsg::Buy {}, + &coins(1000, RESERVE), + &accounts[accounts.len() - 1], + ); + assert_eq!( + result.unwrap_err(), + abc.execute_error(ContractError::SenderNotAllowlisted { + sender: accounts[accounts.len() - 1].address().to_string() + }) + ); + + // Check an address config takes complete priority + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: accounts[0].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(2000u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // The user has already funded 40, so providing their limit should error + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(2000, RESERVE), &accounts[0]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::one(), + max: Uint128::from(2000u128) + }) + ); + + // Funding the remainder is ok + let result = abc.execute(&ExecuteMsg::Buy {}, &coins(1960, RESERVE), &accounts[0]); + assert!(result.is_ok()); +} diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 28dec2752..2ee793794 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -11,12 +11,29 @@ use crate::{ ContractError, }; -use cosmwasm_std::{Coin, Decimal, Uint128}; -use dao_testing::test_tube::cw_tokenfactory_issuer::TokenfactoryIssuer; +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128}; +use cw_utils::Duration; +use dao_interface::{ + state::{Admin, ModuleInstantiateInfo}, + token::{DenomUnit, InitialBalance, NewDenomMetadata, NewTokenInfo}, + voting::DenomResponse, +}; +use dao_testing::test_tube::{ + cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore, + dao_proposal_single::DaoProposalSingle, dao_voting_token_staked::TokenVotingContract, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, +}; +use dao_voting_token_staked::msg::TokenInfo; use osmosis_test_tube::{ - osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest, - osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Bank, Module, - OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, + osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + }, + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, }; use serde::de::DeserializeOwned; use std::fmt::Debug; @@ -51,6 +68,114 @@ impl<'a> TestEnv<'a> { Bank::new(self.app) } + pub fn init_dao_ids(&self) -> (u64, u64) { + ( + TokenVotingContract::upload(self.app, &self.accounts[0]).unwrap(), + DaoProposalSingle::upload(self.app, &self.accounts[0]).unwrap(), + ) + } + + pub fn setup_default_dao(&self, dao_ids: (u64, u64)) -> DaoCore<'a> { + // Only the 1st half of self.accounts are part of the DAO + let initial_balances: Vec = self + .accounts + .iter() + .take(self.accounts.len() / 2) + .map(|acc| InitialBalance { + address: acc.address(), + amount: Uint128::from(100u128), + }) + .collect(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: dao_ids.0, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: self.tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: Some(NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + exponent: 6, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + initial_balances, + initial_dao_balance: Some(Uint128::new(900)), + }), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: dao_ids.1, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + let dao = DaoCore::new(self.app, &msg, &self.accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao + .query(&dao_interface::msg::QueryMsg::VotingModule {}) + .unwrap(); + let vp_contract = + TokenVotingContract::new_with_values(self.app, dao_ids.0, vp_addr.to_string()).unwrap(); + + // Get the denom + let result: RunnerResult = + vp_contract.query(&dao_voting_token_staked::msg::QueryMsg::Denom {}); + let denom = result.unwrap().denom; + + // Stake all members + for acc in self.accounts.iter().take(self.accounts.len() / 2) { + vp_contract + .execute( + &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, + &[Coin::new(100, denom.clone())], + acc, + ) + .unwrap(); + } + + dao + } + pub fn assert_account_balances( &self, account: SigningAccount, @@ -105,13 +230,13 @@ impl TestEnvBuilder { let abc = CwAbc::deploy( app, &InstantiateMsg { - fees_recipient: accounts[0].address(), token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), supply: SupplyToken { subdenom: DENOM.to_string(), metadata: None, decimals: 6, - max_supply: Some(Uint128::from(1000000000u128)), + max_supply: Some(Uint128::from(1_000_000_000u128)), }, reserve: ReserveToken { denom: RESERVE.to_string(), @@ -121,14 +246,13 @@ impl TestEnvBuilder { hatch: HatchConfig { contribution_limits: MinMax { min: Uint128::from(10u128), - max: Uint128::from(1000000u128), + max: Uint128::from(1_000_000u128), }, initial_raise: MinMax { min: Uint128::from(10u128), - max: Uint128::from(1000000u128), + max: Uint128::from(900_000u128), // 1m - 10% }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -169,9 +293,9 @@ impl TestEnvBuilder { let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0])?; - // Override issuer_id and fees_recipient msg.token_issuer_code_id = issuer_id; - msg.fees_recipient = accounts[0].address(); + + msg.funding_pool_forwarding = Some(accounts[0].address()); let abc = CwAbc::deploy(app, &msg, &accounts[0])?; diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index b324ca8ed..4caa65889 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -42,8 +42,8 @@ pub fn default_instantiate_msg( curve_type: CurveType, ) -> InstantiateMsg { InstantiateMsg { - fees_recipient: TEST_CREATOR.to_string(), token_issuer_code_id: 1, + funding_pool_forwarding: None, supply: SupplyToken { subdenom: TEST_SUPPLY_DENOM.to_string(), metadata: Some(default_supply_metadata()), @@ -65,7 +65,6 @@ pub fn default_instantiate_msg( max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::zero(), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), diff --git a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json index 5a5ca6b86..7f7f44465 100644 --- a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json +++ b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json @@ -21,7 +21,7 @@ ], "properties": { "subdenom": { - "description": "component of fulldenom (`factory//`).", + "description": "component of full denom (`factory//`).", "type": "string" } }, @@ -31,7 +31,7 @@ "additionalProperties": false }, { - "description": "`ExistingToken` will use already created token. So to set this up, Token Factory admin for the existing token needs trasfer admin over to this contract, and optionally set the `BeforeSendHook` manually.", + "description": "`ExistingToken` will use already created token. So to set this up, Token Factory admin for the existing token needs transfer admin over to this contract, and optionally set the `BeforeSendHook` manually.", "type": "object", "required": [ "existing_token" @@ -60,7 +60,7 @@ "description": "State changing methods available to this smart contract.", "oneOf": [ { - "description": "Allow adds the target address to the allowlist to be able to send or recieve tokens even if the token is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature to work.\n\nThis functionality is intedended for DAOs who do not wish to have a their tokens liquid while bootstrapping their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens).", + "description": "Allow adds the target address to the allowlist to be able to send or receive tokens even if the token is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature to work.\n\nThis functionality is intended for DAOs who do not wish to have a their tokens liquid while bootstrapping their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens).", "type": "object", "required": [ "allow" @@ -86,7 +86,7 @@ "additionalProperties": false }, { - "description": "Burn token to address. Burn allowance is required and wiil be deducted after successful burn.", + "description": "Burn token to address. Burn allowance is required and will be deducted after successful burn.", "type": "object", "required": [ "burn" @@ -112,7 +112,7 @@ "additionalProperties": false }, { - "description": "Mint token to address. Mint allowance is required and wiil be deducted after successful mint.", + "description": "Mint token to address. Mint allowance is required and will be deducted after successful mint.", "type": "object", "required": [ "mint" @@ -138,7 +138,7 @@ "additionalProperties": false }, { - "description": "Deny adds the target address to the denylist, whis prevents them from sending/receiving the token attached to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this feature to work as intended.", + "description": "Deny adds the target address to the denylist, which prevents them from sending/receiving the token attached to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this feature to work as intended.", "type": "object", "required": [ "deny" @@ -601,7 +601,7 @@ "additionalProperties": false }, { - "description": "Enumerates over all burn allownances. Response: AllowancesResponse", + "description": "Enumerates over all burn allowances. Response: AllowancesResponse", "type": "object", "required": [ "burn_allowances" @@ -653,7 +653,7 @@ "additionalProperties": false }, { - "description": "Enumerates over all mint allownances. Response: AllowancesResponse", + "description": "Enumerates over all mint allowances. Response: AllowancesResponse", "type": "object", "required": [ "mint_allowances" diff --git a/contracts/external/cw-tokenfactory-issuer/src/msg.rs b/contracts/external/cw-tokenfactory-issuer/src/msg.rs index 4587a0ea5..be093bb86 100644 --- a/contracts/external/cw-tokenfactory-issuer/src/msg.rs +++ b/contracts/external/cw-tokenfactory-issuer/src/msg.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] use crate::state::BeforeSendHookInfo; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Coin, Uint128}; @@ -11,11 +12,11 @@ pub enum InstantiateMsg { /// Newly created token will have full denom as `factory//`. /// It will be attached to the contract setup the beforesend listener automatically. NewToken { - /// component of fulldenom (`factory//`). + /// component of full denom (`factory//`). subdenom: String, }, /// `ExistingToken` will use already created token. So to set this up, - /// Token Factory admin for the existing token needs trasfer admin over + /// Token Factory admin for the existing token needs transfer admin over /// to this contract, and optionally set the `BeforeSendHook` manually. ExistingToken { denom: String }, } @@ -23,25 +24,25 @@ pub enum InstantiateMsg { /// State changing methods available to this smart contract. #[cw_serde] pub enum ExecuteMsg { - /// Allow adds the target address to the allowlist to be able to send or recieve tokens even if the token + /// Allow adds the target address to the allowlist to be able to send or receive tokens even if the token /// is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature /// to work. /// - /// This functionality is intedended for DAOs who do not wish to have a their tokens liquid while bootstrapping + /// This functionality is intended for DAOs who do not wish to have a their tokens liquid while bootstrapping /// their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their /// tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens). Allow { address: String, status: bool }, - /// Burn token to address. Burn allowance is required and wiil be deducted after successful burn. + /// Burn token to address. Burn allowance is required and will be deducted after successful burn. Burn { from_address: String, amount: Uint128, }, - /// Mint token to address. Mint allowance is required and wiil be deducted after successful mint. + /// Mint token to address. Mint allowance is required and will be deducted after successful mint. Mint { to_address: String, amount: Uint128 }, - /// Deny adds the target address to the denylist, whis prevents them from sending/receiving the token attached + /// Deny adds the target address to the denylist, which prevents them from sending/receiving the token attached /// to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this /// feature to work as intended. Deny { address: String, status: bool }, @@ -123,7 +124,7 @@ pub enum QueryMsg { #[returns(AllowanceResponse)] BurnAllowance { address: String }, - /// Enumerates over all burn allownances. Response: AllowancesResponse + /// Enumerates over all burn allowances. Response: AllowancesResponse #[returns(AllowancesResponse)] BurnAllowances { start_after: Option, @@ -134,7 +135,7 @@ pub enum QueryMsg { #[returns(AllowanceResponse)] MintAllowance { address: String }, - /// Enumerates over all mint allownances. Response: AllowancesResponse + /// Enumerates over all mint allowances. Response: AllowancesResponse #[returns(AllowancesResponse)] MintAllowances { start_after: Option, @@ -202,7 +203,7 @@ pub struct DenomResponse { } /// Returns the current owner of this issuer contract who is allowed to -/// call priviledged methods. +/// call privileged methods. #[cw_serde] pub struct OwnerResponse { pub address: String, diff --git a/contracts/external/cw-tokenfactory-issuer/src/queries.rs b/contracts/external/cw-tokenfactory-issuer/src/queries.rs index a6b7f3b5d..6e8025c55 100644 --- a/contracts/external/cw-tokenfactory-issuer/src/queries.rs +++ b/contracts/external/cw-tokenfactory-issuer/src/queries.rs @@ -79,7 +79,7 @@ pub fn query_allowances( .collect() } -/// Enumerates over all allownances. Response: AllowancesResponse +/// Enumerates over all allowances. Response: AllowancesResponse pub fn query_mint_allowances( deps: Deps, start_after: Option, @@ -90,7 +90,7 @@ pub fn query_mint_allowances( }) } -/// Enumerates over all burn allownances. Response: AllowancesResponse +/// Enumerates over all burn allowances. Response: AllowancesResponse pub fn query_burn_allowances( deps: Deps, start_after: Option, diff --git a/contracts/external/cw-vesting/README.md b/contracts/external/cw-vesting/README.md index acd62106e..5e5b163fd 100644 --- a/contracts/external/cw-vesting/README.md +++ b/contracts/external/cw-vesting/README.md @@ -34,13 +34,13 @@ It supports 2 types of [curves](https://docs.rs/wynd-utils/0.4.1/wynd_utils/enum ##### Piecewise Linear -Piecsewise Curves can be used to create more complicated vesting +Piecewise Curves can be used to create more complicated vesting schedules. For example, let's say we have a schedule that vests 50% over 1 month and the remaining 50% over 1 year. We can implement this complex schedule with a Piecewise Linear curve. Piecewise Linear curves take a `steps` parameter which is a list of -tuples `(timestamp, vested)`. It will then linearally interpolate +tuples `(timestamp, vested)`. It will then linearly interpolate between those points to create the vesting curve. For example, given the points `(0, 0), (2, 2), (4, 8)`, it would create a vesting curve that looks like this: diff --git a/contracts/external/cw721-roles/src/contract.rs b/contracts/external/cw721-roles/src/contract.rs index 833007e46..ea31936a1 100644 --- a/contracts/external/cw721-roles/src/contract.rs +++ b/contracts/external/cw721-roles/src/contract.rs @@ -319,7 +319,7 @@ pub fn execute_update_token_role( let mut token = contract.tokens.load(deps.storage, &token_id)?; // Update role with new value - token.extension.role = role.clone(); + token.extension.role.clone_from(&role); contract.tokens.save(deps.storage, &token_id, &token)?; Ok(Response::default() @@ -341,7 +341,7 @@ pub fn execute_update_token_uri( let mut token = contract.tokens.load(deps.storage, &token_id)?; // Set new token URI - token.token_uri = token_uri.clone(); + token.token_uri.clone_from(&token_uri); contract.tokens.save(deps.storage, &token_id, &token)?; Ok(Response::new() diff --git a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json index db9be42c1..d1560feea 100644 --- a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -205,7 +205,6 @@ "required": [ "contribution_limits", "entry_fee", - "exit_fee", "initial_raise" ], "properties": { @@ -225,30 +224,108 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/MinMax" } ] - }, - "initial_raise": { - "description": "The initial raise range (min, max) in the reserve token", + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigMsg": { + "type": "object", + "required": [ + "config_type" + ], + "properties": { + "config_type": { + "description": "The type of the configuration", "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ { "$ref": "#/definitions/MinMax" + }, + { + "type": "null" } ] } }, "additionalProperties": false }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntryMsg": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfigMsg" + } + }, + "additionalProperties": false + }, "InstantiateMsg": { "type": "object", "required": [ "curve_type", - "fees_recipient", "phase_config", "reserve", "supply", @@ -263,9 +340,12 @@ } ] }, - "fees_recipient": { - "description": "The recipient for any fees collected from bonding curve operation", - "type": "string" + "funding_pool_forwarding": { + "description": "An optional address for automatically forwarding funding pool gains", + "type": [ + "string", + "null" + ] }, "hatcher_allowlist": { "description": "TODO different ways of doing this, for example DAO members? Using a whitelist contract? Merkle tree? Hatcher allowlist", @@ -274,7 +354,7 @@ "null" ], "items": { - "type": "string" + "$ref": "#/definitions/HatcherAllowlistEntryMsg" } }, "phase_config": { @@ -311,7 +391,7 @@ "additionalProperties": false }, "MinMax": { - "description": "Struct for minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -455,6 +535,10 @@ "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" + }, + "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/external/dao-abc-factory/src/test_tube/test_env.rs b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs index a31a88822..d57f6d57b 100644 --- a/contracts/external/dao-abc-factory/src/test_tube/test_env.rs +++ b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs @@ -135,8 +135,8 @@ impl TestEnvBuilder { contract_addr: dao_abc_factory.contract_addr.clone(), msg: to_json_binary(&ExecuteMsg::AbcFactory { instantiate_msg: cw_abc::msg::InstantiateMsg { - fees_recipient: accounts[0].address(), token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), supply: SupplyToken { subdenom: DENOM.to_string(), metadata: None, @@ -158,7 +158,6 @@ impl TestEnvBuilder { max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -247,8 +246,8 @@ impl TestEnvBuilder { contract_addr: dao_abc_factory.contract_addr.clone(), msg: to_json_binary(&ExecuteMsg::AbcFactory { instantiate_msg: cw_abc::msg::InstantiateMsg { - fees_recipient: accounts[0].address(), token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), supply: SupplyToken { subdenom: DENOM.to_string(), metadata: None, @@ -270,7 +269,6 @@ impl TestEnvBuilder { max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), diff --git a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json index c5203092d..3b6a7e888 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -32,7 +32,7 @@ ] }, "min_voting_period": { - "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker aquires a large number of tokens and forces a proposal through.", + "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker acquires a large number of tokens and forces a proposal through.", "anyOf": [ { "$ref": "#/definitions/Duration" diff --git a/contracts/proposal/dao-proposal-single/src/msg.rs b/contracts/proposal/dao-proposal-single/src/msg.rs index 302303b48..590eb194f 100644 --- a/contracts/proposal/dao-proposal-single/src/msg.rs +++ b/contracts/proposal/dao-proposal-single/src/msg.rs @@ -16,7 +16,7 @@ pub struct InstantiateMsg { /// The minimum amount of time a proposal must be open before /// passing. A proposal may fail before this amount of time has /// elapsed, but it will not pass. This can be useful for - /// preventing governance attacks wherein an attacker aquires a + /// preventing governance attacks wherein an attacker acquires a /// large number of tokens and forces a proposal through. pub min_voting_period: Option, /// If set to true only members may execute passed diff --git a/contracts/proposal/dao-proposal-single/src/proposal.rs b/contracts/proposal/dao-proposal-single/src/proposal.rs index a597f3754..1fd483446 100644 --- a/contracts/proposal/dao-proposal-single/src/proposal.rs +++ b/contracts/proposal/dao-proposal-single/src/proposal.rs @@ -184,7 +184,7 @@ impl SingleChoiceProposal { // and there are possible votes, then this is // rejected if there is a single no vote. // - // We need this check becuase otherwise when + // We need this check because otherwise when // we invert the threshold (`Decimal::one() - // threshold`) we get a 0% requirement for no // votes. Zero no votes do indeed meet a 0% @@ -217,7 +217,7 @@ impl SingleChoiceProposal { // and there are possible votes, then this is // rejected if there is a single no vote. // - // We need this check becuase + // We need this check because // otherwise when we invert the // threshold (`Decimal::one() - // threshold`) we get a 0% requirement @@ -954,7 +954,7 @@ mod test { )); // Total power of 33. 13 total votes. 8 no votes, 3 yes, 2 // abstain. 39.3% turnout. Expired. As it is expired we see if - // the 8 no votes excede the 50% failing threshold, which they + // the 8 no votes exceed the 50% failing threshold, which they // do. assert!(check_is_rejected( quorum.clone(), @@ -980,7 +980,7 @@ mod test { // Over quorum, but under threshold fails if the proposal is // not expired. If the proposal is expired though it passes as // the total vote count used is the number of votes, and not - // the total number of votes avaliable. + // the total number of votes available. assert!(check_is_rejected( quorum.clone(), failing.clone(), diff --git a/contracts/test/dao-proposal-hook-counter/src/msg.rs b/contracts/test/dao-proposal-hook-counter/src/msg.rs index ad6048228..63fed29fe 100644 --- a/contracts/test/dao-proposal-hook-counter/src/msg.rs +++ b/contracts/test/dao-proposal-hook-counter/src/msg.rs @@ -1,4 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +#[allow(unused_imports)] use cosmwasm_std::Uint128; use dao_hooks::{proposal::ProposalHookMsg, stake::StakeChangedHookMsg, vote::VoteHookMsg}; diff --git a/contracts/voting/dao-voting-cw20-staked/src/msg.rs b/contracts/voting/dao-voting-cw20-staked/src/msg.rs index bdb5e9f2a..2fc7b4da5 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/msg.rs @@ -5,6 +5,7 @@ use cw20_base::msg::InstantiateMarketingInfo; use cw_utils::Duration; use dao_dao_macros::{active_query, cw20_token_query, voting_module_query}; +#[allow(unused_imports)] use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; /// Information about the staking contract to be used with this voting diff --git a/contracts/voting/dao-voting-cw721-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index 837851ed3..657dc1085 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -3,6 +3,7 @@ use cosmwasm_std::Binary; use cw721::Cw721ReceiveMsg; use cw_utils::Duration; use dao_dao_macros::{active_query, voting_module_query}; +#[allow(unused_imports)] use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] diff --git a/contracts/voting/dao-voting-token-staked/src/contract.rs b/contracts/voting/dao-voting-token-staked/src/contract.rs index a8d1e2d6d..53aaae888 100644 --- a/contracts/voting/dao-voting-token-staked/src/contract.rs +++ b/contracts/voting/dao-voting-token-staked/src/contract.rs @@ -30,15 +30,15 @@ use dao_voting::{ }, }; -use crate::error::ContractError; use crate::msg::{ ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, - StakerBalanceResponse, TokenInfo, + StakerBalanceResponse, }; use crate::state::{ Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, DENOM, HOOKS, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, }; +use crate::{error::ContractError, msg::TokenInfo}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-token-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -135,10 +135,10 @@ pub fn instantiate( funds, } => { // Call factory contract. Use only a trusted factory contract, - // as this is a critical security component and valdiation of + // as this is a critical security component and validation of // setup will happen in the factory. Ok(Response::new() - .add_attribute("action", "intantiate") + .add_attribute("action", "instantiate") .add_attribute("token", "custom_factory") .add_submessage(SubMsg::reply_on_success( WasmMsg::Execute { diff --git a/contracts/voting/dao-voting-token-staked/src/msg.rs b/contracts/voting/dao-voting-token-staked/src/msg.rs index 98809ec9e..ac194b885 100644 --- a/contracts/voting/dao-voting-token-staked/src/msg.rs +++ b/contracts/voting/dao-voting-token-staked/src/msg.rs @@ -3,6 +3,7 @@ use cosmwasm_std::{Binary, Uint128}; use cw_utils::Duration; use dao_dao_macros::{active_query, native_token_query, voting_module_query}; use dao_interface::token::NewTokenInfo; +#[allow(unused_imports)] use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] diff --git a/packages/cw-curves/Cargo.toml b/packages/cw-curves/Cargo.toml new file mode 100644 index 000000000..87c25b6cd --- /dev/null +++ b/packages/cw-curves/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cw-curves" +authors = [ + "Ethan Frey ", + "Jake Hartnell", + "Adair ", + "Gabe Lopez ", +] +description = "A package for defining curves to be used in augmented bonding curves." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +rust_decimal = { workspace = true } +integer-sqrt = { workspace = true } +integer-cbrt = { workspace = true } \ No newline at end of file diff --git a/packages/cw-curves/README.md b/packages/cw-curves/README.md new file mode 100644 index 000000000..9159a1070 --- /dev/null +++ b/packages/cw-curves/README.md @@ -0,0 +1,6 @@ +# CosmWasm Curves + +This package provides the curves to be used for +[cw-abc](../../contracts/external/cw-abc). + +It provides a framework for defining various types of curves, such as constant, linear, and square root curves. The library ensures precision in token operations allowing for the easy implementation of custom curves in CosmWasm-based applications. diff --git a/packages/cw-curves/src/curve.rs b/packages/cw-curves/src/curve.rs new file mode 100644 index 000000000..002c58019 --- /dev/null +++ b/packages/cw-curves/src/curve.rs @@ -0,0 +1,74 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; + +use crate::utils::decimal; + +/// This defines the curves we are using. +/// +/// I am struggling on what type to use for the math. Tokens are often stored as Uint128, +/// but they may have 6 or 9 digits. When using constant or linear functions, this doesn't matter +/// much, but for non-linear functions a lot more. Also, supply and reserve most likely have different +/// decimals... either we leave it for the callers to normalize and accept a `Decimal` input, +/// or we pass in `Uint128` as well as the decimal places for supply and reserve. +/// +/// After working the first route and realizing that `Decimal` is not all that great to work with +/// when you want to do more complex math than add and multiply `Uint128`, I decided to go the second +/// route. That made the signatures quite complex and my final idea was to pass in `supply_decimal` +/// and `reserve_decimal` in the curve constructors. +pub trait Curve { + /// Returns the spot price given the supply. + /// `f(x)` from the README + fn spot_price(&self, supply: Uint128) -> StdDecimal; + + /// Returns the total price paid up to purchase supply tokens (integral) + /// `F(x)` from the README + fn reserve(&self, supply: Uint128) -> Uint128; + + /// Inverse of reserve. Returns how many tokens would be issued + /// with a total paid amount of reserve. + /// `F^-1(x)` from the README + fn supply(&self, reserve: Uint128) -> Uint128; +} + +/// DecimalPlaces should be passed into curve constructors +#[cw_serde] +#[derive(Copy)] +pub struct DecimalPlaces { + /// Number of decimal places for the supply token (this is what was passed in cw20-base instantiate + pub supply: u32, + /// Number of decimal places for the reserve token (eg. 6 for uatom, 9 for nstep, 18 for wei) + pub reserve: u32, +} + +impl DecimalPlaces { + pub fn new(supply: u8, reserve: u8) -> Self { + DecimalPlaces { + supply: supply as u32, + reserve: reserve as u32, + } + } + + pub fn to_reserve(self, reserve: Decimal) -> Uint128 { + let factor = decimal(10u128.pow(self.reserve), 0); + let out = reserve * factor; + // TODO: execute overflow better? Result? + out.floor().to_u128().unwrap().into() + } + + pub fn to_supply(self, supply: Decimal) -> Uint128 { + let factor = decimal(10u128.pow(self.supply), 0); + let out = supply * factor; + // TODO: execute overflow better? Result? + out.floor().to_u128().unwrap().into() + } + + pub fn from_supply(&self, supply: Uint128) -> Decimal { + decimal(supply, self.supply) + } + + pub fn from_reserve(&self, reserve: Uint128) -> Decimal { + decimal(reserve, self.reserve) + } +} diff --git a/packages/cw-curves/src/curves/constant.rs b/packages/cw-curves/src/curves/constant.rs new file mode 100644 index 000000000..a9fed2310 --- /dev/null +++ b/packages/cw-curves/src/curves/constant.rs @@ -0,0 +1,39 @@ +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::Decimal; + +use crate::{utils::decimal_to_std, Curve, DecimalPlaces}; + +/// spot price is always a constant value +pub struct Constant { + pub value: Decimal, + pub normalize: DecimalPlaces, +} + +impl Constant { + pub fn new(value: Decimal, normalize: DecimalPlaces) -> Self { + Self { value, normalize } + } +} + +impl Curve for Constant { + // we need to normalize value with the reserve decimal places + // (eg 0.1 value would return 100_000 if reserve was uatom) + fn spot_price(&self, _supply: Uint128) -> StdDecimal { + // f(x) = self.value + decimal_to_std(self.value) + } + + /// Returns total number of reserve tokens needed to purchase a given number of supply tokens. + /// Note that both need to be normalized. + fn reserve(&self, supply: Uint128) -> Uint128 { + // f(x) = supply * self.value + let reserve = self.normalize.from_supply(supply) * self.value; + self.normalize.to_reserve(reserve) + } + + fn supply(&self, reserve: Uint128) -> Uint128 { + // f(x) = reserve / self.value + let supply = self.normalize.from_reserve(reserve) / self.value; + self.normalize.to_supply(supply) + } +} diff --git a/packages/cw-curves/src/curves/linear.rs b/packages/cw-curves/src/curves/linear.rs new file mode 100644 index 000000000..c823a25b2 --- /dev/null +++ b/packages/cw-curves/src/curves/linear.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::Decimal; + +use crate::{ + utils::{decimal_to_std, square_root}, + Curve, DecimalPlaces, +}; + +/// spot_price is slope * supply +pub struct Linear { + pub slope: Decimal, + pub normalize: DecimalPlaces, +} + +impl Linear { + pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { + Self { slope, normalize } + } +} + +impl Curve for Linear { + fn spot_price(&self, supply: Uint128) -> StdDecimal { + // f(x) = supply * self.value + let out = self.normalize.from_supply(supply) * self.slope; + decimal_to_std(out) + } + + fn reserve(&self, supply: Uint128) -> Uint128 { + // f(x) = self.slope * supply * supply / 2 + let normalized = self.normalize.from_supply(supply); + let square = normalized * normalized; + // Note: multiplying by 0.5 is much faster than dividing by 2 + let reserve = square * self.slope * Decimal::new(5, 1); + self.normalize.to_reserve(reserve) + } + + fn supply(&self, reserve: Uint128) -> Uint128 { + // f(x) = (2 * reserve / self.slope) ^ 0.5 + // note: use addition here to optimize 2* operation + let square = self.normalize.from_reserve(reserve + reserve) / self.slope; + let supply = square_root(square); + self.normalize.to_supply(supply) + } +} diff --git a/packages/cw-curves/src/curves/mod.rs b/packages/cw-curves/src/curves/mod.rs new file mode 100644 index 000000000..e25a90429 --- /dev/null +++ b/packages/cw-curves/src/curves/mod.rs @@ -0,0 +1,8 @@ +pub mod constant; +pub use constant::Constant; + +pub mod linear; +pub use linear::Linear; + +pub mod square_root; +pub use square_root::SquareRoot; diff --git a/packages/cw-curves/src/curves/square_root.rs b/packages/cw-curves/src/curves/square_root.rs new file mode 100644 index 000000000..539dce07f --- /dev/null +++ b/packages/cw-curves/src/curves/square_root.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::Decimal; + +use crate::{ + utils::{cube_root, decimal_to_std, square_root}, + Curve, DecimalPlaces, +}; + +/// spot_price is slope * (supply)^0.5 +pub struct SquareRoot { + pub slope: Decimal, + pub normalize: DecimalPlaces, +} + +impl SquareRoot { + pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { + Self { slope, normalize } + } +} + +impl Curve for SquareRoot { + fn spot_price(&self, supply: Uint128) -> StdDecimal { + // f(x) = self.slope * supply^0.5 + let square = self.normalize.from_supply(supply); + let root = square_root(square); + decimal_to_std(root * self.slope) + } + + fn reserve(&self, supply: Uint128) -> Uint128 { + // f(x) = self.slope * supply * supply^0.5 / 1.5 + let normalized = self.normalize.from_supply(supply); + let root = square_root(normalized); + let reserve = self.slope * normalized * root / Decimal::new(15, 1); + self.normalize.to_reserve(reserve) + } + + fn supply(&self, reserve: Uint128) -> Uint128 { + // f(x) = (1.5 * reserve / self.slope) ^ (2/3) + let base = self.normalize.from_reserve(reserve) * Decimal::new(15, 1) / self.slope; + let squared = base * base; + let supply = cube_root(squared); + self.normalize.to_supply(supply) + } +} diff --git a/packages/cw-curves/src/lib.rs b/packages/cw-curves/src/lib.rs new file mode 100644 index 000000000..a4b6170a5 --- /dev/null +++ b/packages/cw-curves/src/lib.rs @@ -0,0 +1,8 @@ +pub mod curve; +pub mod curves; +pub mod utils; + +#[cfg(test)] +mod tests; + +pub use curve::{Curve, DecimalPlaces}; diff --git a/packages/cw-curves/src/tests.rs b/packages/cw-curves/src/tests.rs new file mode 100644 index 000000000..97d60c17f --- /dev/null +++ b/packages/cw-curves/src/tests.rs @@ -0,0 +1,118 @@ +// TODO: test DecimalPlaces return proper decimals + +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; + +use crate::{ + curves::{Constant, Linear, SquareRoot}, + utils::decimal, + Curve, DecimalPlaces, +}; + +#[test] +fn constant_curve() { + // supply is nstep (9), reserve is uatom (6) + let normalize = DecimalPlaces::new(9, 6); + let curve = Constant::new(decimal(15u128, 1), normalize); + + // do some sanity checks.... + // spot price is always 1.5 ATOM + assert_eq!( + StdDecimal::percent(150), + curve.spot_price(Uint128::new(123)) + ); + + // if we have 30 STEP, we should have 45 ATOM + let reserve = curve.reserve(Uint128::new(30_000_000_000)); + assert_eq!(Uint128::new(45_000_000), reserve); + + // if we have 36 ATOM, we should have 24 STEP + let supply = curve.supply(Uint128::new(36_000_000)); + assert_eq!(Uint128::new(24_000_000_000), supply); +} + +#[test] +fn linear_curve() { + // supply is usdt (2), reserve is btc (8) + let normalize = DecimalPlaces::new(2, 8); + // slope is 0.1 (eg hits 1.0 after 10btc) + let curve = Linear::new(decimal(1u128, 1), normalize); + + // do some sanity checks.... + // spot price is 0.1 with 1 USDT supply + assert_eq!( + StdDecimal::permille(100), + curve.spot_price(Uint128::new(100)) + ); + // spot price is 1.7 with 17 USDT supply + assert_eq!( + StdDecimal::permille(1700), + curve.spot_price(Uint128::new(1700)) + ); + // spot price is 0.212 with 2.12 USDT supply + assert_eq!( + StdDecimal::permille(212), + curve.spot_price(Uint128::new(212)) + ); + + // if we have 10 USDT, we should have 5 BTC + let reserve = curve.reserve(Uint128::new(1000)); + assert_eq!(Uint128::new(500_000_000), reserve); + // if we have 20 USDT, we should have 20 BTC + let reserve = curve.reserve(Uint128::new(2000)); + assert_eq!(Uint128::new(2_000_000_000), reserve); + + // if we have 1.25 BTC, we should have 5 USDT + let supply = curve.supply(Uint128::new(125_000_000)); + assert_eq!(Uint128::new(500), supply); + // test square root rounding + // TODO: test when supply has many more decimal places than reserve + // if we have 1.11 BTC, we should have 4.7116875957... USDT + let supply = curve.supply(Uint128::new(111_000_000)); + assert_eq!(Uint128::new(471), supply); +} + +#[test] +fn sqrt_curve() { + // supply is utree (6) reserve is chf (2) + let normalize = DecimalPlaces::new(6, 2); + // slope is 0.35 (eg hits 0.35 after 1 chf, 3.5 after 100chf) + let curve = SquareRoot::new(decimal(35u128, 2), normalize); + + // do some sanity checks.... + // spot price is 0.35 with 1 TREE supply + assert_eq!( + StdDecimal::percent(35), + curve.spot_price(Uint128::new(1_000_000)) + ); + // spot price is 3.5 with 100 TREE supply + assert_eq!( + StdDecimal::percent(350), + curve.spot_price(Uint128::new(100_000_000)) + ); + // spot price should be 23.478713763747788 with 4500 TREE supply (test rounding and reporting here) + // rounds off around 8-9 sig figs (note diff for last points) + assert_eq!( + StdDecimal::from_ratio(2347871365u128, 100_000_000u128), + curve.spot_price(Uint128::new(4_500_000_000)) + ); + + // if we have 1 TREE, we should have 0.2333333333333 CHF + let reserve = curve.reserve(Uint128::new(1_000_000)); + assert_eq!(Uint128::new(23), reserve); + // if we have 100 TREE, we should have 233.333333333 CHF + let reserve = curve.reserve(Uint128::new(100_000_000)); + assert_eq!(Uint128::new(23_333), reserve); + // test rounding + // if we have 235 TREE, we should have 840.5790828021146 CHF + let reserve = curve.reserve(Uint128::new(235_000_000)); + assert_eq!(Uint128::new(84_057), reserve); // round down + + // // if we have 0.23 CHF, we should have 0.990453 TREE (round down) + let supply = curve.supply(Uint128::new(23)); + assert_eq!(Uint128::new(990_000), supply); + // if we have 840.58 CHF, we should have 235.000170 TREE (round down) + let supply = curve.supply(Uint128::new(84058)); + assert_eq!(Uint128::new(235_000_000), supply); +} + +// Idea: generic test that curve.supply(curve.reserve(supply)) == supply (or within some small rounding margin) diff --git a/packages/cw-curves/src/utils.rs b/packages/cw-curves/src/utils.rs new file mode 100644 index 000000000..0f8606045 --- /dev/null +++ b/packages/cw-curves/src/utils.rs @@ -0,0 +1,63 @@ +use cosmwasm_std::Decimal as StdDecimal; +use integer_cbrt::IntegerCubeRoot; +use integer_sqrt::IntegerSquareRoot; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; +use std::str::FromStr; + +/// decimal returns an object = num * 10 ^ -scale +/// We use this function in contract.rs rather than call the crate constructor +/// itself, in case we want to swap out the implementation, we can do it only in this file. +pub fn decimal>(num: T, scale: u32) -> Decimal { + Decimal::from_i128_with_scale(num.into() as i128, scale) +} + +/// StdDecimal stores as a u128 with 18 decimal points of precision +pub fn decimal_to_std(x: Decimal) -> StdDecimal { + // this seems straight-forward (if inefficient), converting via string representation + // TODO: execute errors better? Result? + StdDecimal::from_str(&x.to_string()).unwrap() + + // // maybe a better approach doing math, not sure about rounding + // + // // try to preserve decimal points, max 9 + // let digits = min(x.scale(), 9); + // let multiplier = 10u128.pow(digits); + // + // // we multiply up before we round off to u128, + // // let StdDecimal do its best to keep these decimal places + // let nominator = (x * decimal(multiplier, 0)).to_u128().unwrap(); + // StdDecimal::from_ratio(nominator, multiplier) +} + +// we multiply by 10^18, turn to int, take square root, then divide by 10^9 as we convert back to decimal +pub(crate) fn square_root(square: Decimal) -> Decimal { + // must be even + // TODO: this can overflow easily at 18... what is a good value? + const EXTRA_DIGITS: u32 = 12; + let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); + + // multiply by 10^18 and turn to u128 + let extended = square * decimal(multiplier, 0); + let extended = extended.floor().to_u128().unwrap(); + + // take square root, and build a decimal again + let root = extended.integer_sqrt(); + decimal(root, EXTRA_DIGITS / 2) +} + +// we multiply by 10^9, turn to int, take cube root, then divide by 10^3 as we convert back to decimal +pub(crate) fn cube_root(cube: Decimal) -> Decimal { + // must be multiple of 3 + // TODO: what is a good value? + const EXTRA_DIGITS: u32 = 9; + let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); + + // multiply out and turn to u128 + let extended = cube * decimal(multiplier, 0); + let extended = extended.floor().to_u128().unwrap(); + + // take cube root, and build a decimal again + let root = extended.integer_cbrt(); + decimal(root, EXTRA_DIGITS / 3) +} diff --git a/packages/cw-paginate-storage/src/lib.rs b/packages/cw-paginate-storage/src/lib.rs index 61f420f3a..ec1edc410 100644 --- a/packages/cw-paginate-storage/src/lib.rs +++ b/packages/cw-paginate-storage/src/lib.rs @@ -114,7 +114,7 @@ where } /// Same as `paginate_map` but only returns the keys. For use with -/// `SnaphotMap`. +/// `SnapshotMap`. pub fn paginate_snapshot_map_keys<'a, 'b, K, V, R: 'static>( deps: Deps, map: &SnapshotMap<'a, K, V>, diff --git a/packages/dao-interface/src/msg.rs b/packages/dao-interface/src/msg.rs index 969288433..accf36bc9 100644 --- a/packages/dao-interface/src/msg.rs +++ b/packages/dao-interface/src/msg.rs @@ -174,7 +174,7 @@ pub enum QueryMsg { /// Gets the address associated with an item key. #[returns(crate::query::GetItemResponse)] GetItem { key: String }, - /// Lists all of the items associted with the contract. For + /// Lists all of the items associated with the contract. For /// example, given the items `{ "group": "foo", "subdao": "bar"}` /// this query would return `[("group", "foo"), ("subdao", /// "bar")]`. diff --git a/packages/dao-testing/src/test_tube/mod.rs b/packages/dao-testing/src/test_tube/mod.rs index e609fa47e..f7e1cb743 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -2,7 +2,7 @@ // and also, tarpaulin will not be able read coverage out of wasm binary anyway #![cfg(not(tarpaulin))] -// Integrationg tests using an actual chain binary, requires +// Integration tests using an actual chain binary, requires // the "test-tube" feature to be enabled // cargo test --features test-tube #[cfg(feature = "test-tube")] diff --git a/packages/dao-voting/src/voting.rs b/packages/dao-voting/src/voting.rs index 2aabafc2e..48be99a49 100644 --- a/packages/dao-voting/src/voting.rs +++ b/packages/dao-voting/src/voting.rs @@ -179,10 +179,10 @@ impl Votes { /// Computes the total number of votes cast. /// - /// NOTE: The total number of votes avaliable from a voting module + /// NOTE: The total number of votes available from a voting module /// is a `Uint128`. As it is not possible to vote twice we know /// that the sum of votes must be <= 2^128 and can safely return a - /// `Uint128` from this function. A missbehaving voting power + /// `Uint128` from this function. A misbehaving voting power /// module may break this invariant. pub fn total(&self) -> Uint128 { self.yes + self.no + self.abstain