diff --git a/Cargo.toml b/Cargo.toml index 8c825f3..90d9cb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "contracts/multi_staking", "contracts/router", "contracts/governance_admin", + "contracts/superfluid_lp", "packages/*" ] diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt index 0003a02..09f1a02 100644 --- a/artifacts/checksums.txt +++ b/artifacts/checksums.txt @@ -1,16 +1,18 @@ -ec744098d4b8e64b006efa5ea4d38005b66fe6a04868c5731411a5218510d122 dexter_governance_admin-aarch64.wasm -4ae1b5525f1c0a157b9bb681b7cdd96585963c3b7d50b8beb5e40dbb635f4a06 dexter_governance_admin.wasm -f1637c62e407f3a8e632d1646ba059a07574742c66484d22baf2aa9ce53747cd dexter_keeper-aarch64.wasm -eed9ca38fae4f1898b5b9cef3bd6d93353823c139fc81dd502ce3d9bc829a628 dexter_keeper.wasm -c7aca4e73e5f5418d6c9a78e32ff97b3d7a21afe7055a05ff5bc74410e3e367a dexter_multi_staking-aarch64.wasm -010616b1325998fa5ce47437b9c909cb47a6c011282665687aa7e6d7dbc30fdf dexter_multi_staking.wasm -8a55ee7c47ad6e97cef64b2b8ddc7b208c3bfcad870b52c356536739223d4dfe dexter_router-aarch64.wasm -2770cf94f9eb6c25f0f41a122dd83515702ba96e11596dc695d0a58acbbbe5f2 dexter_router.wasm -ccc87e6af394f8d6ed23f3d8501f26bb2e475e2d47e29f516ab46f0b37098b63 dexter_vault-aarch64.wasm -7ce2c09ec05be2e08fdbdbc672f419cf51d8cdab892f35850401b1783fb131f6 dexter_vault.wasm -d1ffada7fa5d2111debc4a0bba615322dc398c17624d7dea68e482aa5bf8d51a lp_token-aarch64.wasm -54ff6959266af5653f95f703e36f1b5d8d72d35b93c535f38cc94b5511634f0c lp_token.wasm -6b83675d1a9b0b34eabea0b4058998a923b836a31310aafccf5c87537b0133ab stable_pool-aarch64.wasm -f457177d505873465476f417e272991876bbb181b812097de8f513ef5938ffaa stable_pool.wasm -6a6a257e606478a66c6dd2db8f37322620ab01e18cbecedcce883985454be770 weighted_pool-aarch64.wasm -0c3ef881f46191127131fe042ca1579f34c20c8af2b93908dd9e14ac46e1249e weighted_pool.wasm +4dab2fc6bd56a50b3765baf335c14ec05b49f4815099e17519f4a7114010d023 dexter_governance_admin-aarch64.wasm +61fd5eb6d634ef8b531298698e4e45424cfd26d92a1228511642608287cddbf4 dexter_governance_admin.wasm +2c6ac09506b69a0ac06ce22079c9477dac495d3f2ff5e4d2c62068887ca0e64e dexter_keeper-aarch64.wasm +2a2c9c0488feba7ac4e9fab0a3bb9b2f1d95a80b2414671593a6dd42b982b4f0 dexter_keeper.wasm +d96e7c699d163ef8f15079ad340015350ac64c84dfef15320815aef6269c67c1 dexter_multi_staking-aarch64.wasm +7cd99e6f45131c63e35949ebd076e5f053d8da065f07fc24550cc3b51b839408 dexter_multi_staking.wasm +68de586576ab108c927259201288f75b00f90276cbc80ba909993981dd425a19 dexter_router-aarch64.wasm +4102b52a873a4fffa114b0c772117c842addc85762362366479099c64d2b69bf dexter_router.wasm +f3046bc7873053f1a64033fe41160a3ac2be874b6ac2f2f1b67b91046bace936 dexter_superfluid_lp-aarch64.wasm +88697e167e6da1af0389c77bd3c740cbb8ffab0e6c79ac8a5b5c86e5b4136d19 dexter_superfluid_lp.wasm +5089880ebaaa47e7d8641368920eeb5298480a1ea025bfc019f501ee5c7e3594 dexter_vault-aarch64.wasm +f86dce8871e7466e2c24922cd753812830277dd8372e3002a94cecd476d5bb9a dexter_vault.wasm +45a289fd2342621e0dbe9d2c6193536be7e7a17843cacb56e392a94ce5a62dcb lp_token-aarch64.wasm +b944e64e40cbea733c247e8bfeed7329ea2159bd295dde402a485d61e81ba1aa lp_token.wasm +dbcb817d905d9ccc1183c62715f32e9592981cbf5329fe529353b8d12a2f8316 stable_pool-aarch64.wasm +0d2a3990e5b0fdd6276fe330adb2ed38c1235150b2acebc2be2f734ccf1dfc89 stable_pool.wasm +08436930bb4abe2cb78f5f7ae8583410b10fc3eef57e6bc9e6e021f61ef739d8 weighted_pool-aarch64.wasm +755d6a4c78ea6f1579260b7b61b52545d11f2b90c149838eee078924453a274a weighted_pool.wasm diff --git a/artifacts/checksums_intermediate.txt b/artifacts/checksums_intermediate.txt index 990f9c5..19d26eb 100644 --- a/artifacts/checksums_intermediate.txt +++ b/artifacts/checksums_intermediate.txt @@ -1,8 +1,9 @@ -b712ae72f337b514b412980b318d6354e0dd3ab8e33cc87e0da94d78f0e875d7 target/wasm32-unknown-unknown/release/dexter_governance_admin.wasm -73c81bb6a9e418e0c5eaca3f38efcf9687ed39d20e39f3aad7648b52fd66277b target/wasm32-unknown-unknown/release/dexter_keeper.wasm -d5fedf51f7b10a4d2705331e053efe8999148421b1f2ba8bee577082cf4ff9a9 target/wasm32-unknown-unknown/release/dexter_multi_staking.wasm -29b9ab70ff4438b6fc2394c87f3965106ba313590e5c9b6bc85d937a9cefa41d target/wasm32-unknown-unknown/release/dexter_router.wasm -52026056c605a76edf8e20197badae509076117c6684db72c94d3af3ef614cc7 target/wasm32-unknown-unknown/release/dexter_vault.wasm -9bd1325f5a7460e8857d86525cd24c6354684f500c580112782a96d23e914cd4 target/wasm32-unknown-unknown/release/lp_token.wasm -688fc035141cf14036baf03fc665ff1eb7f789f04152e7b46555c525d3401af1 target/wasm32-unknown-unknown/release/stable_pool.wasm -705ff501c260f21bc07d4ea39d702afb980cbb81c679e490a1bc8f311fb35849 target/wasm32-unknown-unknown/release/weighted_pool.wasm +6864211daf765d04b611daed7e4aa91fa1594f8c65eb1a1669e065ce4f9504f2 target/wasm32-unknown-unknown/release/dexter_governance_admin.wasm +337c3945a6fd4f80383370664b7a10fae5bcc2ffa665de30e51cf1fe81e9a4fd target/wasm32-unknown-unknown/release/dexter_keeper.wasm +ac31cf4d60749229327a513a630efe212096a1de1d5af4da0c78beb46dca2087 target/wasm32-unknown-unknown/release/dexter_multi_staking.wasm +fb899ce3aa92829910f318462dfe735303023e76cd76a24189a6bb45f06472ca target/wasm32-unknown-unknown/release/dexter_router.wasm +7304a5cacdf40b2fca1f5d69bd1504167905bd94083df06a31014fc10d52ed28 target/wasm32-unknown-unknown/release/dexter_superfluid_lp.wasm +6b5dc74e43757d44d35bab7ae7f3a4807a836540a4f675b2dd35f8715ab63864 target/wasm32-unknown-unknown/release/dexter_vault.wasm +5e37e85f31a5c762543e16159c3bba55413b1436937690465166fb4ce96558a4 target/wasm32-unknown-unknown/release/lp_token.wasm +4725f4f4d7b910a529182fc47b0b2cfd47ef1343e4652ce6d5449941d7a021e3 target/wasm32-unknown-unknown/release/stable_pool.wasm +ed142f681bfe3b3bba578ccfc3af05009cb3d03147fb3fa9561edabc78dcf270 target/wasm32-unknown-unknown/release/weighted_pool.wasm diff --git a/artifacts/dexter_governance_admin-aarch64.wasm b/artifacts/dexter_governance_admin-aarch64.wasm index 74cf7d4..70bfdae 100644 Binary files a/artifacts/dexter_governance_admin-aarch64.wasm and b/artifacts/dexter_governance_admin-aarch64.wasm differ diff --git a/artifacts/dexter_governance_admin.wasm b/artifacts/dexter_governance_admin.wasm index c5252c8..0284e1a 100644 Binary files a/artifacts/dexter_governance_admin.wasm and b/artifacts/dexter_governance_admin.wasm differ diff --git a/artifacts/dexter_keeper-aarch64.wasm b/artifacts/dexter_keeper-aarch64.wasm index 0a5ab65..2f9da8f 100644 Binary files a/artifacts/dexter_keeper-aarch64.wasm and b/artifacts/dexter_keeper-aarch64.wasm differ diff --git a/artifacts/dexter_keeper.wasm b/artifacts/dexter_keeper.wasm index 1f1535a..5dc0b9d 100644 Binary files a/artifacts/dexter_keeper.wasm and b/artifacts/dexter_keeper.wasm differ diff --git a/artifacts/dexter_multi_staking-aarch64.wasm b/artifacts/dexter_multi_staking-aarch64.wasm index d053868..53dbaf3 100644 Binary files a/artifacts/dexter_multi_staking-aarch64.wasm and b/artifacts/dexter_multi_staking-aarch64.wasm differ diff --git a/artifacts/dexter_multi_staking.wasm b/artifacts/dexter_multi_staking.wasm index 3d4d2cf..ba2b1ea 100644 Binary files a/artifacts/dexter_multi_staking.wasm and b/artifacts/dexter_multi_staking.wasm differ diff --git a/artifacts/dexter_router-aarch64.wasm b/artifacts/dexter_router-aarch64.wasm index e2397e6..764ed1e 100644 Binary files a/artifacts/dexter_router-aarch64.wasm and b/artifacts/dexter_router-aarch64.wasm differ diff --git a/artifacts/dexter_router.wasm b/artifacts/dexter_router.wasm index 9886163..f56b2c3 100644 Binary files a/artifacts/dexter_router.wasm and b/artifacts/dexter_router.wasm differ diff --git a/artifacts/dexter_superfluid_lp-aarch64.wasm b/artifacts/dexter_superfluid_lp-aarch64.wasm new file mode 100644 index 0000000..e51f576 Binary files /dev/null and b/artifacts/dexter_superfluid_lp-aarch64.wasm differ diff --git a/artifacts/dexter_superfluid_lp.wasm b/artifacts/dexter_superfluid_lp.wasm new file mode 100644 index 0000000..476c709 Binary files /dev/null and b/artifacts/dexter_superfluid_lp.wasm differ diff --git a/artifacts/dexter_vault-aarch64.wasm b/artifacts/dexter_vault-aarch64.wasm index 796321d..ce3cf2f 100644 Binary files a/artifacts/dexter_vault-aarch64.wasm and b/artifacts/dexter_vault-aarch64.wasm differ diff --git a/artifacts/dexter_vault.wasm b/artifacts/dexter_vault.wasm index f792a79..0a7b9bf 100644 Binary files a/artifacts/dexter_vault.wasm and b/artifacts/dexter_vault.wasm differ diff --git a/artifacts/lp_token-aarch64.wasm b/artifacts/lp_token-aarch64.wasm index afe0612..dfdbdc7 100644 Binary files a/artifacts/lp_token-aarch64.wasm and b/artifacts/lp_token-aarch64.wasm differ diff --git a/artifacts/lp_token.wasm b/artifacts/lp_token.wasm index 6d6b3da..139a483 100644 Binary files a/artifacts/lp_token.wasm and b/artifacts/lp_token.wasm differ diff --git a/artifacts/stable_pool-aarch64.wasm b/artifacts/stable_pool-aarch64.wasm index d8f0232..13f6797 100644 Binary files a/artifacts/stable_pool-aarch64.wasm and b/artifacts/stable_pool-aarch64.wasm differ diff --git a/artifacts/stable_pool.wasm b/artifacts/stable_pool.wasm index c5a1a0e..f54067e 100644 Binary files a/artifacts/stable_pool.wasm and b/artifacts/stable_pool.wasm differ diff --git a/artifacts/weighted_pool-aarch64.wasm b/artifacts/weighted_pool-aarch64.wasm index 473586c..6706220 100644 Binary files a/artifacts/weighted_pool-aarch64.wasm and b/artifacts/weighted_pool-aarch64.wasm differ diff --git a/artifacts/weighted_pool.wasm b/artifacts/weighted_pool.wasm index 2b62de8..a1b26fd 100644 Binary files a/artifacts/weighted_pool.wasm and b/artifacts/weighted_pool.wasm differ diff --git a/contracts/governance_admin/schema/dexter-governance-admin.json b/contracts/governance_admin/schema/dexter-governance-admin.json index 3e2627b..1bf6489 100644 --- a/contracts/governance_admin/schema/dexter-governance-admin.json +++ b/contracts/governance_admin/schema/dexter-governance-admin.json @@ -1,6 +1,6 @@ { "contract_name": "dexter-governance-admin", - "contract_version": "1.0.0", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/superfluid_lp/.cargo/config b/contracts/superfluid_lp/.cargo/config new file mode 100644 index 0000000..b80cc3e --- /dev/null +++ b/contracts/superfluid_lp/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +wasm-debug = "build --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test dexter_superfluid_lp" +schema = "run --example dexter_superfluid_lp" \ No newline at end of file diff --git a/contracts/superfluid_lp/Cargo.toml b/contracts/superfluid_lp/Cargo.toml new file mode 100644 index 0000000..6de0a38 --- /dev/null +++ b/contracts/superfluid_lp/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "dexter-superfluid-lp" +version = "1.0.0" +authors = ["PersistenceLabs"] +edition = "2021" +description = "Superfluid LP contract to facilitate staked token -> LST -> LP 1-click conversion" +license = "MIT" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] + + +[dependencies] +dexter = { path = "../../packages/dexter", default-features = false } +cw20 = "1.0.1" +cw2 = "1.0.1" +cw20-base = { version = "1.0.1", features = ["library"] } +cosmwasm-std = "1.5.0" +cw-storage-plus = "1.0.1" +schemars = "0.8.11" +thiserror = "1.0.38" +serde = { version = "1.0.152", default-features = false, features = ["derive"] } +serde-json-wasm = "0.5.0" +cosmwasm-schema = "1.5.0" +const_format = "0.2.30" +cw-utils = "1.0.3" + +[dev-dependencies] +# we only need to enable this if we use integration tests +cw-multi-test = "0.16.2" +dexter-vault = { path = "../vault"} +dexter-multi-staking = { path = "../multi_staking"} +stable-pool = { path = "../pools/stable_pool" } +weighted-pool = { path = "../pools/weighted_pool"} +lp-token = { path = "../lp_token"} +cw20 = "1.0.1" diff --git a/contracts/superfluid_lp/README.md b/contracts/superfluid_lp/README.md new file mode 100644 index 0000000..a051cfb --- /dev/null +++ b/contracts/superfluid_lp/README.md @@ -0,0 +1,104 @@ +# Dexter Superfluid LP contract + +Dexter superfluid LP contract enables 1-click LP position from staked chain tokens directly to a bonded LP position. + +This is part of the stkXPRT rollout. For more details, see [here]() + +## Roles + +**Owner**: Manages the contract admin parameters. Currently the only admin parameter is the vault contract address. + +**User**: Person who locks LST tokens in the contract and then uses them to create a bonded LP position. + + +## Supported state transition functions + +### Owner executable + +Following transition functions can only be executed by the `owner` of the contract. + +#### 1. _**Update config**_ + +Updates the contract admin parameters. + +```json +{ + "update_config": { + "vault_address": "persistence1..." + } +} +``` + +#### 2. _**Add allowed lockable token**_ + +Adds a new token denom to the list of allowed lockable tokens. This is used to whitelist tokens that can be locked in the contract. + +```json +{ + "add_allowed_lockable_token": { + "native_token": { + "denom": "stk/uxprt" + } + } +} +``` + +### User executable + +#### 1. _**Lock LST**_ + +Locks LST tokens in the contract which can then be used to create a bonded LP position. This is ideally part of a superfluid LP transaction. + +Only a set of whitelisted token denoms an be locked in the contract. These can be added by the contract owner. + +This calls from the liquidstake module once the LST has been minted for the user, to lock it in the same message. + +Post this, another messages can be sent, ideally in the same transaction to use the locked LST to create a bonded LP position using the correspondng transition function. + +```json +{ + "lock_lst": { + "asset": { + "amount": "1000000000000000000000000", + "info": { + "native_token": { + "denom": "stk/uxprt" + } + } + } + } +} +``` + +#### 2. _**Join pool and bond using Locked LST**_ + +Allows a user who locked his LST to use any combination of the locked LST and extra tokens that he sent to add liquidity to a pool and also bond it. Ideally, this message is part of a single transaction that converts a staked asset to LST and then lock it using the `lock_lst` transition function. + + +```json +{ + "join_pool_and_bond_using_locked_lst": { + "pool_id": "1", + "total_assets": [ + { + "amount": "900000000", + "info": { + "native_token": { + "denom": "stk/uxprt" + } + } + }, + { + "amount": "1000000000", + "info": { + "token": { + "contract_addr": "persistence1..." + } + } + } + ], + "min_lp_to_receive": "100000000" + } +} +``` + diff --git a/contracts/superfluid_lp/examples/dexter_superfluid_lp.rs b/contracts/superfluid_lp/examples/dexter_superfluid_lp.rs new file mode 100644 index 0000000..cc7dec7 --- /dev/null +++ b/contracts/superfluid_lp/examples/dexter_superfluid_lp.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use dexter::router::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/superfluid_lp/schema/dexter-superfluid-lp.json b/contracts/superfluid_lp/schema/dexter-superfluid-lp.json new file mode 100644 index 0000000..f438445 --- /dev/null +++ b/contracts/superfluid_lp/schema/dexter-superfluid-lp.json @@ -0,0 +1,666 @@ +{ + "contract_name": "dexter-superfluid-lp", + "contract_version": "1.0.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "dexter_vault" + ], + "properties": { + "dexter_vault": { + "description": "The dexter Vault contract address", + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "ExecuteMultihopSwap processes multiple swaps via dexter pools", + "type": "object", + "required": [ + "execute_multihop_swap" + ], + "properties": { + "execute_multihop_swap": { + "type": "object", + "required": [ + "offer_amount", + "requests" + ], + "properties": { + "minimum_receive": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "offer_amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "requests": { + "type": "array", + "items": { + "$ref": "#/definitions/HopSwapRequest" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Callbacks; only callable by the contract itself.", + "type": "object", + "required": [ + "callback" + ], + "properties": { + "callback": { + "$ref": "#/definitions/CallbackMsg" + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types.", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "CallbackMsg": { + "oneOf": [ + { + "type": "object", + "required": [ + "continue_hop_swap" + ], + "properties": { + "continue_hop_swap": { + "type": "object", + "required": [ + "minimum_receive", + "offer_asset", + "prev_ask_amount", + "recipient", + "requests" + ], + "properties": { + "minimum_receive": { + "$ref": "#/definitions/Uint128" + }, + "offer_asset": { + "$ref": "#/definitions/AssetInfo" + }, + "prev_ask_amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "$ref": "#/definitions/Addr" + }, + "requests": { + "type": "array", + "items": { + "$ref": "#/definitions/HopSwapRequest" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HopSwapRequest": { + "description": "This enum describes a swap operation.", + "type": "object", + "required": [ + "asset_in", + "asset_out", + "pool_id" + ], + "properties": { + "asset_in": { + "description": "The offer asset", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "asset_out": { + "description": "The ask asset", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "pool_id": { + "description": "Pool Id via which the swap is to be routed", + "allOf": [ + { + "$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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "SimulateMultihopSwap simulates multi-hop swap operations", + "type": "object", + "required": [ + "simulate_multihop_swap" + ], + "properties": { + "simulate_multihop_swap": { + "type": "object", + "required": [ + "amount", + "multiswap_request", + "swap_type" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "multiswap_request": { + "type": "array", + "items": { + "$ref": "#/definitions/HopSwapRequest" + } + }, + "swap_type": { + "$ref": "#/definitions/SwapType" + } + }, + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types.", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HopSwapRequest": { + "description": "This enum describes a swap operation.", + "type": "object", + "required": [ + "asset_in", + "asset_out", + "pool_id" + ], + "properties": { + "asset_in": { + "description": "The offer asset", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "asset_out": { + "description": "The ask asset", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "pool_id": { + "description": "Pool Id via which the swap is to be routed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false + }, + "SwapType": { + "description": "This enum describes available Swap types.", + "oneOf": [ + { + "type": "object", + "required": [ + "give_in" + ], + "properties": { + "give_in": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "give_out" + ], + "properties": { + "give_out": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom swap type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "type": "object", + "required": [ + "dexter_vault" + ], + "properties": { + "dexter_vault": { + "description": "The dexter vault contract address", + "type": "string" + } + }, + "additionalProperties": false + }, + "simulate_multihop_swap": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SimulateMultiHopResponse", + "type": "object", + "required": [ + "fee", + "response", + "swap_operations" + ], + "properties": { + "fee": { + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "response": { + "$ref": "#/definitions/ResponseType" + }, + "swap_operations": { + "type": "array", + "items": { + "$ref": "#/definitions/SimulatedTrade" + } + } + }, + "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" + }, + "Asset": { + "title": "Description - This enum describes a asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types.", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ResponseType": { + "title": "Description", + "description": "This enum is used to describe if the math computations (joins/exits/swaps) will be successful or not", + "oneOf": [ + { + "type": "object", + "required": [ + "success" + ], + "properties": { + "success": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "failure" + ], + "properties": { + "failure": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "SimulatedTrade": { + "type": "object", + "required": [ + "asset_in", + "asset_out", + "offered_amount", + "pool_id", + "received_amount" + ], + "properties": { + "asset_in": { + "$ref": "#/definitions/AssetInfo" + }, + "asset_out": { + "$ref": "#/definitions/AssetInfo" + }, + "offered_amount": { + "$ref": "#/definitions/Uint128" + }, + "pool_id": { + "$ref": "#/definitions/Uint128" + }, + "received_amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/superfluid_lp/src/contract.rs b/contracts/superfluid_lp/src/contract.rs new file mode 100644 index 0000000..b257b92 --- /dev/null +++ b/contracts/superfluid_lp/src/contract.rs @@ -0,0 +1,410 @@ +use std::collections::HashMap; +use std::vec; + +use crate::error::ContractError; +use crate::state::{CONFIG, OWNERSHIP_PROPOSAL, LOCK_AMOUNT}; +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, + Response, Uint128, WasmMsg, StdError, Coin, +}; +use cw2::set_contract_version; +use cw20::Cw20ExecuteMsg; +use dexter::asset::{Asset, AssetInfo}; +use dexter::helper::{build_transfer_cw20_from_user_msg, propose_new_owner, claim_ownership, drop_ownership_proposal, build_transfer_token_to_user_msg}; +use dexter::superfluid_lp::{ExecuteMsg, InstantiateMsg, QueryMsg, Config}; +use dexter::vault::ExecuteMsg as VaultExecuteMsg; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "dexter-superfluid-lp"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// ----------------x----------------x----------------x----------------x----------------x---------------- +// ----------------x----------------x Instantiate Contract : Execute function x---------------- +// ----------------x----------------x----------------x----------------x----------------x---------------- + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // validate vault address + deps.api.addr_validate(&msg.vault_addr.to_string())?; + + // validate owner address + deps.api.addr_validate(&msg.owner.to_string())?; + + let mut all_denoms = vec![]; + // validate that all assets are native tokens + for allowed_asset in &msg.allowed_lockable_tokens { + // validate that no token is a cw20 token. + match &allowed_asset { + AssetInfo::NativeToken { denom } => { + // check no duplicate denoms + if all_denoms.contains(denom) { + // reject duplicate denoms + return Err(ContractError::DuplicateDenom); + } + all_denoms.push(denom.clone()); + } + AssetInfo::Token { contract_addr: _ } => { + // we don't support cw20 tokens for now. + return Err(ContractError::UnsupportedAssetType) + } + } + } + + let config = Config { + vault_addr: msg.vault_addr, + owner: msg.owner, + allowed_lockable_tokens: msg.allowed_lockable_tokens, + }; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::TotalAmountLocked { user, asset_info } => { + + let locked_amount = LOCK_AMOUNT + .may_load(_deps.storage, (&user, &asset_info.to_string()))? + .unwrap_or_default(); + + Ok(to_json_binary(&locked_amount)?) + }, + + QueryMsg::Config {} => { + let config = CONFIG.load(_deps.storage)?; + Ok(to_json_binary(&config)?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::LockLstAsset { asset} => { + + let user = info.sender.clone(); + + // validate that the asset is allowed to be locked. + let config = CONFIG.load(deps.storage)?; + let mut allowed = false; + for allowed_asset in config.allowed_lockable_tokens { + if allowed_asset == asset.info { + allowed = true; + break; + } + } + + if !allowed { + return Err(ContractError::AssetNotAllowedToBeLocked); + } + + let mut locked_amount: Uint128 = LOCK_AMOUNT + .may_load(deps.storage, (&user, &asset.info.to_string()))? + .unwrap_or_default(); + + // confirm that this asset was sent along with the message. We only support native assets. + match &asset.info { + AssetInfo::NativeToken { denom } => { + let amount = cw_utils::must_pay(&info, denom).map_err(|e| ContractError::PaymentError(e))?; + if amount != asset.amount { + return Err(ContractError::InvalidAmount); + } + } + AssetInfo::Token { contract_addr: _ } => { + return Err(ContractError::UnsupportedAssetType); + } + } + + // add the amount to the locked amount + locked_amount = locked_amount + asset.amount; + + // update locked amount + LOCK_AMOUNT.save(deps.storage, (&user, &asset.info.to_string()), &locked_amount)?; + Ok(Response::default()) + } + + ExecuteMsg::JoinPoolAndBondUsingLockedLst { + pool_id, + total_assets, + min_lp_to_receive, + } => { + let config = CONFIG.load(deps.storage)?; + + join_pool_and_bond_using_locked_lst( + deps, + env, + info, + config.vault_addr.to_string(), + pool_id, + total_assets, + min_lp_to_receive, + ) + } + + ExecuteMsg::UpdateConfig { vault_addr } => { + // validate that the message sender is the owner of the contract. + if info.sender != CONFIG.load(deps.storage)?.owner { + return Err(ContractError::Unauthorized {}); + } + + let mut config = CONFIG.load(deps.storage)?; + + if let Some(vault_addr) = vault_addr { + // validate vault address + deps.api.addr_validate(&vault_addr.to_string())?; + config.vault_addr = vault_addr; + } + + CONFIG.save(deps.storage, &config)?; + Ok(Response::default()) + }, + + ExecuteMsg::AddAllowedLockableToken { asset_info } => { + // validate that the message sender is the owner of the contract. + if info.sender != CONFIG.load(deps.storage)?.owner { + return Err(ContractError::Unauthorized {}); + } + + // validate that the token is native + match &asset_info { + AssetInfo::NativeToken { denom: _ } => {} + AssetInfo::Token { contract_addr: _ } => { + return Err(ContractError::UnsupportedAssetType); + } + } + + let mut config = CONFIG.load(deps.storage)?; + + // validate that the token is not already in the list of allowed lockable tokens. + for allowed_asset in &config.allowed_lockable_tokens { + if allowed_asset == &asset_info { + return Err(ContractError::AssetAlreadyAllowedToBeLocked); + } + } + + config.allowed_lockable_tokens.push(asset_info); + + CONFIG.save(deps.storage, &config)?; + Ok(Response::default()) + } + + ExecuteMsg::RemoveAllowedLockableToken { asset_info } => { + // validate that the message sender is the owner of the contract. + if info.sender != CONFIG.load(deps.storage)?.owner { + return Err(ContractError::Unauthorized {}); + } + + let mut config = CONFIG.load(deps.storage)?; + + // validate that the token is in the list of allowed lockable tokens. + let mut found = false; + for (i, allowed_asset) in config.allowed_lockable_tokens.iter().enumerate() { + if allowed_asset == &asset_info { + found = true; + config.allowed_lockable_tokens.remove(i); + break; + } + } + + if !found { + return Err(ContractError::AssetNotInAllowedList); + } + + CONFIG.save(deps.storage, &config)?; + Ok(Response::default()) + } + + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config = CONFIG.load(deps.storage)?; + propose_new_owner( + deps, + info, + env, + owner.to_string(), + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + CONTRACT_NAME + ) + .map_err(|e| e.into()) + }, + + ExecuteMsg::DropOwnershipProposal {} => { + let config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL, CONTRACT_NAME) + .map_err(|e| e.into()) + }, + + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG.update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + })?; + + Ok(()) + }, CONTRACT_NAME) + .map_err(|e| e.into()) + } + } +} + +fn join_pool_and_bond_using_locked_lst( + deps: DepsMut, + env: Env, + info: MessageInfo, + vault_addr: String, + pool_id: Uint128, + total_assets: Vec, + min_lp_to_receive: Option, +) -> Result { + let mut response = Response::new(); + let mut msgs = vec![]; + + let funds = info.funds; + let funds_map: HashMap = funds + .into_iter() + .map(|asset| (asset.denom, asset.amount)) + .collect(); + + let mut unspent_sent_funds = funds_map.clone(); + + let mut coins = vec![]; + + // check if the user has enough balance to join the pool. + for asset in &total_assets { + let total_amount_to_spend = asset.amount; + + match &asset.info { + AssetInfo::NativeToken { denom } => { + let amount_sent = funds_map.get(denom).cloned().unwrap_or(Uint128::zero()); + + if amount_sent == total_amount_to_spend { + // do nothing + unspent_sent_funds.remove(denom); + } + // if amount sent is already bigger than the amount to spend, then we don't need to unlock any tokens for the user. + // we can return any extra tokens back to the user. + else if amount_sent > total_amount_to_spend { + let extra_amount = amount_sent.checked_sub(total_amount_to_spend).unwrap(); + unspent_sent_funds.insert(denom.clone(), extra_amount); + } + // else check if we need to unlock some amount for the user. + else { + + // remove from the unspent funds map. + unspent_sent_funds.remove(denom); + + let amount_to_unlock = total_amount_to_spend.checked_sub(amount_sent).unwrap(); + + let mut locked_amount = LOCK_AMOUNT + .may_load(deps.storage, (&info.sender, &asset.info.to_string()))? + .unwrap_or_default(); + + let total_spendable_amount = amount_sent + locked_amount; + + // we can spend upto the total spendable amount for the user. + if total_amount_to_spend > total_spendable_amount { + return Err(ContractError::InsufficientBalance { + denom: denom.clone(), + available_balance: total_spendable_amount, + required_balance: total_amount_to_spend, + }); + } + + locked_amount = locked_amount.checked_sub(amount_to_unlock)?; + + LOCK_AMOUNT.save( + deps.storage, + (&info.sender, &asset.info.to_string()), + &locked_amount, + )?; + } + + // add to coins vec + let coin = Coin { + denom: denom.clone(), + amount: total_amount_to_spend, + }; + + coins.push(coin); + } + AssetInfo::Token { contract_addr } => { + // create a message to send the tokens from the user to the contract. + let msg = build_transfer_cw20_from_user_msg( + contract_addr.to_string(), + info.sender.to_string(), + env.contract.address.to_string(), + asset.amount, + )?; + + msgs.push(msg); + + // Add another message to allow spending of the tokens from the current contract to the vault. + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: vault_addr.clone(), + amount: asset.amount, + expires: None, + }; + + let wasm_msg: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_json_binary(&msg)?, + funds: vec![], + }); + + msgs.push(wasm_msg); + } + } + } + + // return unspent funds back to the user. + for (denom, amount) in unspent_sent_funds { + let msg = build_transfer_token_to_user_msg( + AssetInfo::NativeToken { denom: denom.clone() }, + info.sender.clone(), + amount, + )?; + + msgs.push(msg); + } + + // create a message to join the pool. + let join_pool_msg = VaultExecuteMsg::JoinPool { + pool_id, + recipient: Some(info.sender.to_string()), + assets: Some(total_assets.clone()), + min_lp_to_receive, + auto_stake: Some(true), + }; + + let wasm_msg: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: vault_addr.to_string(), + msg: to_json_binary(&join_pool_msg)?, + funds: coins + }); + + + msgs.push(wasm_msg); + + response = response.add_messages(msgs); + return Ok(response); + +} \ No newline at end of file diff --git a/contracts/superfluid_lp/src/error.rs b/contracts/superfluid_lp/src/error.rs new file mode 100644 index 0000000..d28a558 --- /dev/null +++ b/contracts/superfluid_lp/src/error.rs @@ -0,0 +1,52 @@ +use cosmwasm_std::{ConversionOverflowError, OverflowError, StdError, Uint128}; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Insuffient balance")] + InsufficientBalance { denom: String, available_balance: Uint128, required_balance: Uint128 }, + + #[error("Unauthorized to perform this action")] + Unauthorized, + + #[error("Unsupported asset type")] + UnsupportedAssetType, + + #[error("Invalid amount sent")] + InvalidAmount, + + #[error("Not implemented")] + NotImplemented, + + #[error("Only whitelisted assets can be locked")] + AssetNotAllowedToBeLocked, + + #[error("Asset is not currently allowed")] + AssetNotInAllowedList, + + #[error("Asset is already allowed to be locked")] + AssetAlreadyAllowedToBeLocked, + + #[error("Payment error: {0}")] + PaymentError(PaymentError), + + #[error("Duplicate denom")] + DuplicateDenom, + +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} + +impl From for ContractError { + fn from(o: ConversionOverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/contracts/superfluid_lp/src/lib.rs b/contracts/superfluid_lp/src/lib.rs new file mode 100644 index 0000000..3d3e89c --- /dev/null +++ b/contracts/superfluid_lp/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod state; diff --git a/contracts/superfluid_lp/src/state.rs b/contracts/superfluid_lp/src/state.rs new file mode 100644 index 0000000..e192419 --- /dev/null +++ b/contracts/superfluid_lp/src/state.rs @@ -0,0 +1,12 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Map, Item}; +use dexter::{superfluid_lp::Config, helper::OwnershipProposal}; + + +// Stores the amount of LST tokens that are locked for the user +pub const LOCK_AMOUNT: Map<(&Addr, &String), Uint128> = Map::new("lock_amount"); + +pub const CONFIG: Item = Item::new("config"); + +// Ownership Proposal currently active in the Vault in a [`OwnershipProposal`] struct +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); \ No newline at end of file diff --git a/contracts/superfluid_lp/tests/integration.rs b/contracts/superfluid_lp/tests/integration.rs new file mode 100644 index 0000000..8b4be64 --- /dev/null +++ b/contracts/superfluid_lp/tests/integration.rs @@ -0,0 +1,552 @@ +use cosmwasm_std::testing::mock_env; +use cosmwasm_std::{Addr, Coin, Timestamp, Uint128, to_json_binary}; +use cw20::MinterResponse; +use cw_multi_test::{App, ContractWrapper, Executor}; +use dexter::asset::{Asset, AssetInfo}; +use dexter::vault::{FeeInfo, PauseInfo, PoolCreationFee, PoolTypeConfig, NativeAssetPrecisionInfo}; + +const EPOCH_START: u64 = 1_000_000; + +#[macro_export] +macro_rules! uint128_with_precision { + ($value:expr, $precision:expr) => { + cosmwasm_std::Uint128::from($value) + .checked_mul(cosmwasm_std::Uint128::from(10u64).pow($precision as u32)) + .unwrap() + }; +} + +fn mock_app(owner: Addr, coins: Vec) -> App { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(EPOCH_START); + + let mut app = App::new(|router, _, storage| { + // initialization moved to App construction + router.bank.init_balance(storage, &owner, coins).unwrap(); + }); + app.set_block(env.block); + app +} + +fn store_multi_staking_code(app: &mut App) -> u64 { + let multi_staking_contract = Box::new(ContractWrapper::new_with_empty( + dexter_multi_staking::contract::execute, + dexter_multi_staking::contract::instantiate, + dexter_multi_staking::contract::query, + )); + app.store_code(multi_staking_contract) +} + +fn store_vault_code(app: &mut App) -> u64 { + let dexter_vault = Box::new( + ContractWrapper::new_with_empty( + dexter_vault::contract::execute, + dexter_vault::contract::instantiate, + dexter_vault::contract::query, + ) + .with_reply_empty(dexter_vault::contract::reply), + ); + + app.store_code(dexter_vault) +} + +fn store_weighted_pool_code(app: &mut App) -> u64 { + let weighted_pool_contract = Box::new(ContractWrapper::new_with_empty( + weighted_pool::contract::execute, + weighted_pool::contract::instantiate, + weighted_pool::contract::query, + )); + app.store_code(weighted_pool_contract) +} + +fn store_token_code(app: &mut App) -> u64 { + let token_contract = Box::new(ContractWrapper::new_with_empty( + lp_token::contract::execute, + lp_token::contract::instantiate, + lp_token::contract::query, + )); + app.store_code(token_contract) +} + +fn store_superfluid_lp_code(app: &mut App) -> u64 { + let superfluid_lp_contract = Box::new(ContractWrapper::new_with_empty( + dexter_superfluid_lp::contract::execute, + dexter_superfluid_lp::contract::instantiate, + dexter_superfluid_lp::contract::query, + )); + app.store_code(superfluid_lp_contract) +} + +pub fn create_cw20_token(app: &mut App, code_id: u64, sender: Addr, token_name: String) -> Addr { + let lp_token_instantiate_msg = dexter::lp_token::InstantiateMsg { + name: token_name, + symbol: "abcde".to_string(), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: sender.to_string(), + cap: None, + }), + marketing: None, + }; + + let lp_token_instance = app + .instantiate_contract( + code_id, + sender.clone(), + &lp_token_instantiate_msg, + &[], + "lp_token", + Some(sender.to_string()), + ) + .unwrap(); + + return lp_token_instance; +} + +fn instantiate_contract(app: &mut App, owner: &Addr) -> (Addr, Addr, Addr) { + let token_code_id = store_token_code(app); + let multistaking_code = store_multi_staking_code(app); + let superfluid_lp_code = store_superfluid_lp_code(app); + let vault_code = store_vault_code(app); + let weighted_pool_code = store_weighted_pool_code(app); + + let keeper = String::from("keeper"); + + let keeper_addr = Addr::unchecked(keeper.clone()); + + // instantiate multistaking contract + let msg = dexter::multi_staking::InstantiateMsg { + owner: owner.clone(), + unlock_period: 1000, + minimum_reward_schedule_proposal_start_delay: 3 * 24 * 60 * 60, + keeper_addr: keeper_addr.clone(), + instant_unbond_fee_bp: 500, + instant_unbond_min_fee_bp: 200, + fee_tier_interval: 1000, + }; + + let multi_staking_instance = app + .instantiate_contract( + multistaking_code, + owner.clone(), + &msg, + &[], + "multi_staking", + None, + ) + .unwrap(); + + let vault_instantiate_msg = dexter::vault::InstantiateMsg { + owner: owner.to_string(), + pool_configs: vec![PoolTypeConfig { + code_id: weighted_pool_code, + pool_type: dexter::vault::PoolType::Weighted {}, + default_fee_info: FeeInfo { + total_fee_bps: 30, + protocol_fee_percent: 30, + }, + allow_instantiation: dexter::vault::AllowPoolInstantiation::Everyone, + paused: PauseInfo { + deposit: false, + imbalanced_withdraw: true, + swap: false, + }, + }], + lp_token_code_id: Some(token_code_id), + fee_collector: None, + pool_creation_fee: PoolCreationFee::Disabled, + auto_stake_impl: dexter::vault::AutoStakeImpl::Multistaking { + contract_addr: multi_staking_instance.clone(), + }, + }; + + let vault_instance = app + .instantiate_contract( + vault_code, + owner.clone(), + &vault_instantiate_msg, + &[], + "vault", + None, + ) + .unwrap(); + + let superfluid_lp_instantiate_msg = dexter::superfluid_lp::InstantiateMsg { + vault_addr: vault_instance.clone(), + owner: owner.clone(), + allowed_lockable_tokens: vec![ + AssetInfo::NativeToken { + denom: "stk/uxprt".to_string(), + }, + ], + }; + + let superfluid_lp_instance = app + .instantiate_contract( + superfluid_lp_code, + owner.clone(), + &superfluid_lp_instantiate_msg, + &[], + "superfluid_lp", + None, + ) + .unwrap(); + + return ( + multi_staking_instance, + superfluid_lp_instance, + vault_instance + ); +} + +#[test] +fn test_superfluid_lp_locking() { + let coins = vec![ + Coin { + denom: "stk/uxprt".to_string(), + amount: Uint128::from(100000000000u128), + }, + Coin { + denom: "uxprt".to_string(), + amount: Uint128::from(100000000000u128), + }, + Coin { + denom: "ustake".to_string(), + amount: Uint128::from(100000000000u128), + } + ]; + + let owner = Addr::unchecked("owner"); + let mut app = mock_app(owner.clone(), coins); + let (multi_staking_instance, superfluid_lp_instance, vault_instance) = + instantiate_contract(&mut app, &Addr::unchecked("owner")); + + // create a CW20 token + let cw20_code_id = store_token_code(&mut app); + + // let's assume that there is a CW20 token which is also an LST for XPRT, and let's have a 3 Pool with XPRT and LST as assets + let cw20_lst_addr = create_cw20_token(&mut app, cw20_code_id, owner.clone(), "Staked XPRT".to_string()); + + + let create_pool_msg = dexter::vault::ExecuteMsg::CreatePoolInstance { + pool_type: dexter::vault::PoolType::Weighted {}, + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uxprt".to_string(), + }, + AssetInfo::NativeToken { + denom: "stk/uxprt".to_string(), + }, + AssetInfo::Token { + contract_addr: cw20_lst_addr.clone(), + }, + ], + native_asset_precisions: vec![ + NativeAssetPrecisionInfo { + denom: "uxprt".to_string(), + precision: 6, + }, + NativeAssetPrecisionInfo { + denom: "stk/uxprt".to_string(), + precision: 6, + } + ], + fee_info: Some(FeeInfo { + total_fee_bps: 30, + protocol_fee_percent: 30, + }), + init_params: to_json_binary( + &weighted_pool::state::WeightedParams { + weights: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uxprt".to_string(), + }, + amount: Uint128::from(1u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "stk/uxprt".to_string(), + }, + amount: Uint128::from(1u128), + }, + Asset { + info: AssetInfo::Token { + contract_addr: cw20_lst_addr.clone(), + }, + amount: Uint128::from(1u128), + }, + ], + exit_fee: None, + } + ).ok() + }; + + app.execute_contract( + owner.clone(), + vault_instance.clone(), + &create_pool_msg, + &[], + ).unwrap(); + + // get LP token address for the pool + let query_msg = dexter::vault::QueryMsg::GetPoolById { + pool_id: Uint128::from(1u128), + }; + + let res: dexter::vault::PoolInfoResponse = app + .wrap() + .query_wasm_smart(vault_instance.clone(), &query_msg) + .unwrap(); + + + let lp_token_addr = res.lp_token_addr; + + // allow LP token in the multistaking contract + let msg = dexter::multi_staking::ExecuteMsg::AllowLpToken { + lp_token: lp_token_addr.clone(), + }; + + app.execute_contract( + owner.clone(), + multi_staking_instance.clone(), + &msg, + &[], + ).unwrap(); + + + let user = String::from("user"); + let user_addr = Addr::unchecked(user.clone()); + + // mint Cw20 tokens for the user + let mint_msg = cw20::Cw20ExecuteMsg::Mint { + recipient: user_addr.to_string(), + amount: Uint128::from(10000000u128), + }; + + app.execute_contract( + owner.clone(), + cw20_lst_addr.clone(), + &mint_msg, + &[], + ).unwrap(); + + // send XPRT and stkXPRT to the user + app.send_tokens( + owner, + user_addr.clone(), + &[ + Coin { + denom: "uxprt".to_string(), + amount: Uint128::from(20000000u128), + }, + Coin { + denom: "stk/uxprt".to_string(), + amount: Uint128::from(10000000u128), + }, + Coin { + denom: "ustake".to_string(), + amount: Uint128::from(10000000u128), + } + ], + ).unwrap(); + + let invalid_msg = dexter::superfluid_lp::ExecuteMsg::LockLstAsset { + asset: Asset { + info: AssetInfo::NativeToken { + denom: "uxprt".to_string(), + }, + amount: Uint128::from(10000000u128), + }, + }; + + // locking of uxprt should fail since it's not allowed + let res = app + .execute_contract( + user_addr.clone(), + superfluid_lp_instance.clone(), + &invalid_msg, + &[], + ); + + assert!(res.is_err()); + assert_eq!(res.unwrap_err().root_cause().to_string(), "Only whitelisted assets can be locked"); + + // locking for CW20 staked XPRT should fail since it's not allowed + + let invalid_msg = dexter::superfluid_lp::ExecuteMsg::LockLstAsset { + asset: Asset { + info: AssetInfo::Token { + contract_addr: cw20_lst_addr.clone(), + }, + amount: Uint128::from(10000000u128), + }, + }; + + let res = app + .execute_contract( + user_addr.clone(), + superfluid_lp_instance.clone(), + &invalid_msg, + &[], + ); + + assert!(res.is_err()); + assert_eq!(res.unwrap_err().root_cause().to_string(), "Only whitelisted assets can be locked"); + + let msg = dexter::superfluid_lp::ExecuteMsg::LockLstAsset { + asset: Asset { + info: AssetInfo::NativeToken { + denom: "stk/uxprt".to_string(), + }, + amount: Uint128::from(10000000u128), + }, + }; + + app + .execute_contract( + user_addr.clone(), + superfluid_lp_instance.clone(), + &msg, + &[Coin { + denom: "stk/uxprt".to_string(), + amount: Uint128::from(10000000u128), + }], + ) + .unwrap(); + + // query the locked tokens + let query_msg = dexter::superfluid_lp::QueryMsg::TotalAmountLocked { + user: Addr::unchecked(user.clone()), + asset_info: AssetInfo::NativeToken { + denom: "stk/uxprt".to_string(), + }, + }; + + let res: Uint128 = app + .wrap() + .query_wasm_smart(superfluid_lp_instance.clone(), &query_msg) + .unwrap(); + + assert_eq!(res, Uint128::from(10000000u128)); + + // join pool using the locked tokens + let join_pool_msg = dexter::superfluid_lp::ExecuteMsg::JoinPoolAndBondUsingLockedLst { + pool_id: Uint128::from(1u128), + total_assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "stk/uxprt".to_string(), + }, + amount: Uint128::from(10000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uxprt".to_string(), + }, + amount: Uint128::from(10000000u128), + }, + Asset { + info: AssetInfo::Token { + contract_addr: cw20_lst_addr.clone(), + }, + amount: Uint128::from(10000000u128), + }, + ], + min_lp_to_receive: None, + }; + + let response = app + .execute_contract( + user_addr.clone(), + superfluid_lp_instance.clone(), + &join_pool_msg, + // add funds to the message + &[Coin { + denom: "uxprt".to_string(), + amount: Uint128::from(10000000u128), + }], + ); + + // confirm error + assert!(response.is_err()); + // error should be about the spending of CW20 tokens + assert_eq!(response.unwrap_err().root_cause().to_string(), "No allowance for this account"); + + // set allowance and try again + let allowance_msg = cw20::Cw20ExecuteMsg::IncreaseAllowance { + spender: superfluid_lp_instance.to_string(), + amount: Uint128::from(10000000u128), + expires: None, + }; + + app.execute_contract( + user_addr.clone(), + cw20_lst_addr.clone(), + &allowance_msg, + &[], + ).unwrap(); + + // query and store uxprt and ustable balances for the user + let uxprt_balance = app + .wrap() + .query_balance(user_addr.clone(), "uxprt".to_string()).unwrap(); + + let ustake_balance = app + .wrap() + .query_balance(user_addr.clone(), "ustake".to_string()).unwrap(); + + // try again + let response = app + .execute_contract( + user_addr.clone(), + superfluid_lp_instance.clone(), + &join_pool_msg, + // add funds to the message + &[ + // send extra XPRT and make sure it returns the extra XPRT back + Coin { + denom: "uxprt".to_string(), + amount: Uint128::from(12000000u128), + }, + // add an extra denom to the message, and make sure it returns the extra denom back + Coin { + denom: "ustake".to_string(), + amount: Uint128::from(10000000u128), + } + ], + ); + + // confirm success + assert!(response.is_ok()); + + // validate no extra uxprt was spent + let uxprt_balance_after = app + .wrap() + .query_balance(user_addr.clone(), "uxprt".to_string()).unwrap(); + + assert_eq!(uxprt_balance_after.amount, uxprt_balance.amount - Uint128::from(10000000u128)); + + // validate no extra ustake was spent + let ustake_balance_after = app + .wrap() + .query_balance(user_addr.clone(), "ustake".to_string()).unwrap(); + + assert_eq!(ustake_balance_after.amount, ustake_balance.amount); + + // confirm LP tokens are minted for the user and bonded + let query_msg = dexter::multi_staking::QueryMsg::BondedLpTokens { + lp_token: lp_token_addr.clone(), + user: user_addr.clone(), + }; + + let res: Uint128 = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query_msg) + .unwrap(); + + // validate that it must be equal to 100 (Decimal 18) since that's the default amount of LP tokens minted for the first time user + assert_eq!(res, uint128_with_precision!(100u128, 18)); + +} diff --git a/packages/dexter/Cargo.toml b/packages/dexter/Cargo.toml index 6a53ac9..e5a4585 100644 --- a/packages/dexter/Cargo.toml +++ b/packages/dexter/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "dexter" -version = "1.2.0" -authors = ["AstroTechLabs"] +version = "1.3.0" +authors = ["PersistenceLabs"] edition = "2021" -description = "DEX on Persistence tendermint chain" -repository = "https://github.com/persistenceOne/dexter" +description = "Dex optimized for liquid staked assets" +repository = "https://github.com/dexter-zone/dexter_core" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,7 +16,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cw20 = "1.0.1" cw20-base = { version = "1.0.1", features = ["library"] } -cosmwasm-std = "1.2.1" +cosmwasm-std = "1.5.3" schemars = "0.8.11" serde = { version = "1.0.152", default-features = false, features = ["derive"] } cw-storage-plus = "1.0.1" diff --git a/packages/dexter/src/lib.rs b/packages/dexter/src/lib.rs index 9efdb87..9413e95 100644 --- a/packages/dexter/src/lib.rs +++ b/packages/dexter/src/lib.rs @@ -9,6 +9,7 @@ pub mod pool; pub mod querier; pub mod router; pub mod vault; +pub mod superfluid_lp; pub mod constants; #[allow(clippy::all)] diff --git a/packages/dexter/src/superfluid_lp.rs b/packages/dexter/src/superfluid_lp.rs new file mode 100644 index 0000000..cbd15b9 --- /dev/null +++ b/packages/dexter/src/superfluid_lp.rs @@ -0,0 +1,92 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128}; + +use crate::asset::{Asset, AssetInfo}; + +#[cw_serde] +pub struct InstantiateMsg { + pub vault_addr: Addr, + // allowed lockable tokens + pub allowed_lockable_tokens: Vec, + /// owner of the contract + pub owner: Addr, +} + +#[cw_serde] +#[derive(Eq)] +pub struct LockInfo { + pub amount: Uint128, + pub unlock_time: u64, +} + +#[cw_serde] +pub struct Config { + pub vault_addr: Addr, + pub owner: Addr, + pub allowed_lockable_tokens: Vec +} + + +#[cw_serde] +pub enum ExecuteMsg { + + /// Locks an LST asset for the user, which can only be used to join a pool on Dexter + /// Kept this API to support CW20 LST tokens too which isn't an immediate usecase but might be useful in the future + LockLstAsset { + asset: Asset, + }, + + /// Join pool on behalf of the user using the locked LST and any extra tokens that the user is willing to spend. + /// This function will also bond the LP position for the user so they immediately start earning the LP incentive rewards. + JoinPoolAndBondUsingLockedLst { + pool_id: Uint128, + /// Represents the total amount of each token that the user wants to spend for joining the pool + /// This includes the amount of tokens that are locked for the user + any extra token that he is willing to send along and spend + total_assets: Vec, + min_lp_to_receive: Option + }, + + /// Update config + UpdateConfig { + vault_addr: Option, + }, + + // add a new token to the list of allowed lockable tokens + AddAllowedLockableToken { + asset_info: AssetInfo + }, + + // remove a token from the list of allowed lockable tokens + RemoveAllowedLockableToken { + asset_info: AssetInfo + }, + + /// Allows the owner to transfer ownership to a new address. + /// Ownership transfer is done in two steps: + /// 1. The owner proposes a new owner. + /// 2. The new owner accepts the ownership. + /// The proposal expires after a certain period of time within which the new owner must accept the ownership. + ProposeNewOwner { + owner: Addr, + expires_in: u64, + }, + /// Allows the new owner to accept ownership. + ClaimOwnership {}, + /// Allows the owner to drop the ownership transfer proposal. + DropOwnershipProposal {} +} + + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Uint128)] + TotalAmountLocked { + user: Addr, + asset_info: AssetInfo + }, + + #[returns(Config)] + Config {} + +} \ No newline at end of file