diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt index 09f1a02..493675e 100644 --- a/artifacts/checksums.txt +++ b/artifacts/checksums.txt @@ -1,18 +1,18 @@ -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 +21355a578e0ef0861f4b9c23e70f54e90847137ad7cf550bdc60ad762c4df8e3 dexter_governance_admin-aarch64.wasm +378459f0abdc57f4cadb8692be2352cea64acc16d2fc77d104f2d1467939401d dexter_governance_admin.wasm +ca6a64f456f478973cfd9b3bc99c2725f6f5c182f9f4df9005d7f0436661aa5d dexter_keeper-aarch64.wasm +50242640f23b46c1f0276f479a2852fac30ea386c590c17f7493b4529112c870 dexter_keeper.wasm +6422ec98a4996161f687380bcca826b02fe26f402a20620246a18ea21229420f dexter_multi_staking-aarch64.wasm +09c374819a3e57ea0b9da6be5c54d061b6d5838d2e943fce7096af3db5c00de8 dexter_multi_staking.wasm +1f2687ed6d8d91595c3251abd4514c09749bebf6b9ef8cbd94753bcdc467e218 dexter_router-aarch64.wasm +48b97784bc431744d2f727941da939ce6cf04c49959b97595dc431b080070cf9 dexter_router.wasm +b448981216b66c02f592c4edc8afb95caa563d9191876d8f71532f8387df70f3 dexter_superfluid_lp-aarch64.wasm +803214635c6b618c4395206bb54f0f5f4d861c90096f064272d1dabf7fa1e3cc dexter_superfluid_lp.wasm +f08b0b2809e99e8ae65ec28293a097a6faca78e5a4530ba94d498382a32ae9ae dexter_vault-aarch64.wasm +0c15b06cfb0f52b779b79fd4b56f784babcb172b75c35ac4b83f3b0fdf7acdba dexter_vault.wasm +9c1ef4d27c2f76a968c909ffdc6739a794d871594151e49e931a48da1d87bdc2 lp_token-aarch64.wasm +9633cca272749e266be93aba1a597d3fe49d0a84e7ab842d7a34402c01f8bef5 lp_token.wasm +5e35bfe3d8e3c5f89f585c04b2353893162cac2d299c132c2bccb494a25ee7a7 stable_pool-aarch64.wasm +730fe353b08d71a33bd12694064338dc78373a35fe39a7575ad5ed79d389b821 stable_pool.wasm +7aea930f4bcb0da4e94dc173c0528185138671b2d868cf679ce42b9ca90a53fa weighted_pool-aarch64.wasm +7019ffc8f3eacdfcc13d588d26de784204f729fa2898ba1ff2cf5bb1e7bee586 weighted_pool.wasm diff --git a/artifacts/checksums_intermediate.txt b/artifacts/checksums_intermediate.txt index 19d26eb..89af001 100644 --- a/artifacts/checksums_intermediate.txt +++ b/artifacts/checksums_intermediate.txt @@ -1,9 +1,8 @@ -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 +391c353d650626d40011385a2fe50fb506a09d2472a6c54da94d885cd1ac52bf target/wasm32-unknown-unknown/release/dexter_governance_admin.wasm +13fe7953094edaf64dd46dd832be31541557a93d3601f6017315d227fc8b9a27 target/wasm32-unknown-unknown/release/dexter_keeper.wasm +2465ac00de52d10397e64142799751ca33cd6a387b348e462d43fc79192cf1c7 target/wasm32-unknown-unknown/release/dexter_multi_staking.wasm +d7fce682716cc1e79e2d370da5544fd0840e4bbf75f3c9d8c7a34c55d0fa713f target/wasm32-unknown-unknown/release/dexter_router.wasm +add05a4995b70b780078908bfbe9e6754ad06c2ce5793f93595d252ad4121ea7 target/wasm32-unknown-unknown/release/dexter_vault.wasm +00ba3921736b1f788e079c87085199289e5dc3c7801c7762c24674603f933551 target/wasm32-unknown-unknown/release/lp_token.wasm +936645177e3da336e4689d535cfdc25c48f5723b0d4b34b42d38783b2fd3f7b9 target/wasm32-unknown-unknown/release/stable_pool.wasm +f99881e3c5db034788179720ed70afffafc0d3651eb156a8f24bfb4a5048e850 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 70bfdae..47fe256 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 0284e1a..97a5763 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 2f9da8f..7086bd8 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 5dc0b9d..ccfdfe2 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 53dbaf3..bd0ab2a 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 ba2b1ea..dcb51a9 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 764ed1e..f4929b0 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 f56b2c3..02160f7 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 index e51f576..bd7f076 100644 Binary files a/artifacts/dexter_superfluid_lp-aarch64.wasm and b/artifacts/dexter_superfluid_lp-aarch64.wasm differ diff --git a/artifacts/dexter_superfluid_lp.wasm b/artifacts/dexter_superfluid_lp.wasm index 476c709..b366a42 100644 Binary files a/artifacts/dexter_superfluid_lp.wasm and b/artifacts/dexter_superfluid_lp.wasm differ diff --git a/artifacts/dexter_vault-aarch64.wasm b/artifacts/dexter_vault-aarch64.wasm index ce3cf2f..813ff51 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 0a7b9bf..752824b 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 dfdbdc7..61675c1 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 139a483..d6d91e9 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 13f6797..c931e58 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 f54067e..181e237 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 6706220..196a521 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 a1b26fd..88407e7 100644 Binary files a/artifacts/weighted_pool.wasm and b/artifacts/weighted_pool.wasm differ diff --git a/contracts/governance_admin/tests/utils/mod.rs b/contracts/governance_admin/tests/utils/mod.rs index bcb8c89..d6198c6 100644 --- a/contracts/governance_admin/tests/utils/mod.rs +++ b/contracts/governance_admin/tests/utils/mod.rs @@ -304,15 +304,18 @@ pub fn setup_test_contracts() -> GovAdminTestSetup { .data .address; - // instante the multistaking contract + // instantiate the multistaking contract let multi_staking_instantiate = dexter::multi_staking::InstantiateMsg { owner: Addr::unchecked(gov_admin_instance.clone()), - unlock_period: 86400u64, keeper_addr: Addr::unchecked(gov_admin_instance.clone()), - minimum_reward_schedule_proposal_start_delay: 0, - instant_unbond_fee_bp: 500u64, - instant_unbond_min_fee_bp: 200u64, - fee_tier_interval: 86400u64, + unbond_config: dexter::multi_staking::UnbondConfig { + unlock_period: 86400u64, + instant_unbond_config: dexter::multi_staking::InstantUnbondConfig::Enabled { + min_fee: 200u64, + max_fee: 500u64, + fee_tier_interval: 86400u64, + }, + }, }; let multi_staking_instance = wasm diff --git a/contracts/multi_staking/Cargo.toml b/contracts/multi_staking/Cargo.toml index d383329..882ae32 100644 --- a/contracts/multi_staking/Cargo.toml +++ b/contracts/multi_staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dexter-multi-staking" -version = "3.0.0" +version = "3.1.0" authors = ["Persistence Labs"] edition = "2021" diff --git a/contracts/multi_staking/schema/dexter-multi-staking.json b/contracts/multi_staking/schema/dexter-multi-staking.json index 60d3d67..07cd679 100644 --- a/contracts/multi_staking/schema/dexter-multi-staking.json +++ b/contracts/multi_staking/schema/dexter-multi-staking.json @@ -1,52 +1,25 @@ { "contract_name": "dexter-multi-staking", - "contract_version": "3.0.0", + "contract_version": "3.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", "required": [ - "fee_tier_interval", - "instant_unbond_fee_bp", - "instant_unbond_min_fee_bp", "keeper_addr", - "minimum_reward_schedule_proposal_start_delay", "owner", - "unlock_period" + "unbond_config" ], "properties": { - "fee_tier_interval": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_fee_bp": { - "description": "value between 0 and 1000 (0% to 10%) are allowed", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_min_fee_bp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, "keeper_addr": { "$ref": "#/definitions/Addr" }, - "minimum_reward_schedule_proposal_start_delay": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, "owner": { "$ref": "#/definitions/Addr" }, - "unlock_period": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "unbond_config": { + "$ref": "#/definitions/UnbondConfig" } }, "additionalProperties": false, @@ -54,6 +27,78 @@ "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" + }, + "InstantUnbondConfig": { + "oneOf": [ + { + "type": "string", + "enum": [ + "disabled" + ] + }, + { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "object", + "required": [ + "fee_tier_interval", + "max_fee", + "min_fee" + ], + "properties": { + "fee_tier_interval": { + "description": "This is the interval period in seconds on which we will have fee tier boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "max_fee": { + "description": "Instant LP unbonding fee. This is the percentage of the LP tokens that will be deducted as fee value between 0 and 1000 (0% to 10%) are allowed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "min_fee": { + "description": "This is the minimum fee charged for instant LP unlock when the unlock time is less than fee interval in future. Fee in between the unlock duration and fee tier intervals will be linearly interpolated at fee tier interval boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "UnbondConfig": { + "type": "object", + "required": [ + "instant_unbond_config", + "unlock_period" + ], + "properties": { + "instant_unbond_config": { + "description": "Status of instant unbonding", + "allOf": [ + { + "$ref": "#/definitions/InstantUnbondConfig" + } + ] + }, + "unlock_period": { + "description": "Unlocking period in seconds This is the minimum time that must pass before a user can withdraw their staked tokens and rewards after they have called the unbond function", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -71,30 +116,6 @@ "update_config": { "type": "object", "properties": { - "fee_tier_interval": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_fee_bp": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_min_fee_bp": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, "keeper_addr": { "anyOf": [ { @@ -105,13 +126,63 @@ } ] }, - "unlock_period": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 + "unbond_config": { + "anyOf": [ + { + "$ref": "#/definitions/UnbondConfig" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Add custom unbdond config for a given LP token", + "type": "object", + "required": [ + "set_custom_unbond_config" + ], + "properties": { + "set_custom_unbond_config": { + "type": "object", + "required": [ + "lp_token", + "unbond_config" + ], + "properties": { + "lp_token": { + "$ref": "#/definitions/Addr" + }, + "unbond_config": { + "$ref": "#/definitions/UnbondConfig" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Unset custom unbdond config for a given LP token", + "type": "object", + "required": [ + "unset_custom_unbond_config" + ], + "properties": { + "unset_custom_unbond_config": { + "type": "object", + "required": [ + "lp_token" + ], + "properties": { + "lp_token": { + "$ref": "#/definitions/Addr" } }, "additionalProperties": false @@ -212,6 +283,50 @@ }, "additionalProperties": false }, + { + "description": "Add reward CW20 token to the list of allowed reward tokens", + "type": "object", + "required": [ + "allow_reward_cw20_token" + ], + "properties": { + "allow_reward_cw20_token": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove reward CW20 token from the list of allowed reward tokens", + "type": "object", + "required": [ + "remove_reward_cw20_token" + ], + "properties": { + "remove_reward_cw20_token": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Allows the contract to receive CW20 tokens. The contract can receive CW20 tokens from LP tokens for staking and CW20 assets to be used as rewards.", "type": "object", @@ -494,6 +609,54 @@ }, "additionalProperties": false }, + "InstantUnbondConfig": { + "oneOf": [ + { + "type": "string", + "enum": [ + "disabled" + ] + }, + { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "object", + "required": [ + "fee_tier_interval", + "max_fee", + "min_fee" + ], + "properties": { + "fee_tier_interval": { + "description": "This is the interval period in seconds on which we will have fee tier boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "max_fee": { + "description": "Instant LP unbonding fee. This is the percentage of the LP tokens that will be deducted as fee value between 0 and 1000 (0% to 10%) are allowed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "min_fee": { + "description": "This is the minimum fee charged for instant LP unlock when the unlock time is less than fee interval in future. Fee in between the unlock duration and fee tier intervals will be linearly interpolated at fee tier interval boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "TokenLock": { "type": "object", "required": [ @@ -515,6 +678,30 @@ "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" + }, + "UnbondConfig": { + "type": "object", + "required": [ + "instant_unbond_config", + "unlock_period" + ], + "properties": { + "instant_unbond_config": { + "description": "Status of instant unbonding", + "allOf": [ + { + "$ref": "#/definitions/InstantUnbondConfig" + } + ] + }, + "unlock_period": { + "description": "Unlocking period in seconds This is the minimum time that must pass before a user can withdraw their staked tokens and rewards after they have called the unbond function", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -536,6 +723,32 @@ }, "additionalProperties": false }, + { + "description": "Returns current unbond config of a given LP token (or global)", + "type": "object", + "required": [ + "unbond_config" + ], + "properties": { + "unbond_config": { + "type": "object", + "properties": { + "lp_token": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns currently unclaimed rewards for a user for a give LP token If a future block time is provided, it will return the unclaimed rewards till that block time.", "type": "object", @@ -693,6 +906,27 @@ ], "properties": { "instant_unlock_fee_tiers": { + "type": "object", + "required": [ + "lp_token" + ], + "properties": { + "lp_token": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "default_instant_unlock_fee_tiers" + ], + "properties": { + "default_instant_unlock_fee_tiers": { "type": "object", "additionalProperties": false } @@ -917,17 +1151,36 @@ "title": "MigrateMsg", "oneOf": [ { + "description": "Removes the reward schedule proposal start delay config param Instant unbonding fee and keeper address are added", "type": "object", "required": [ - "v3_from_v2" + "v3_1_from_v1" ], "properties": { - "v3_from_v2": { + "v3_1_from_v1": { "type": "object", "required": [ + "fee_tier_interval", + "instant_unbond_fee_bp", + "instant_unbond_min_fee_bp", "keeper_addr" ], "properties": { + "fee_tier_interval": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "instant_unbond_fee_bp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "instant_unbond_min_fee_bp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "keeper_addr": { "$ref": "#/definitions/Addr" } @@ -940,51 +1193,45 @@ { "type": "object", "required": [ - "v3_from_v2_2" + "v3_1_from_v2" ], "properties": { - "v3_from_v2_2": { + "v3_1_from_v2": { "type": "object", + "required": [ + "keeper_addr" + ], + "properties": { + "keeper_addr": { + "$ref": "#/definitions/Addr" + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Removes the reward schedule proposal start delay config param Instant unbonding fee and keeper address are added", "type": "object", "required": [ - "v3_from_v1" + "v3_1_from_v2_2" ], "properties": { - "v3_from_v1": { + "v3_1_from_v2_2": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "v3_1_from_v3" + ], + "properties": { + "v3_1_from_v3": { "type": "object", - "required": [ - "fee_tier_interval", - "instant_unbond_fee_bp", - "instant_unbond_min_fee_bp", - "keeper_addr" - ], - "properties": { - "fee_tier_interval": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_fee_bp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_min_fee_bp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "keeper_addr": { - "$ref": "#/definitions/Addr" - } - }, "additionalProperties": false } }, @@ -1026,12 +1273,10 @@ "type": "object", "required": [ "allowed_lp_tokens", - "fee_tier_interval", - "instant_unbond_fee_bp", - "instant_unbond_min_fee_bp", + "allowed_reward_cw20_tokens", "keeper", "owner", - "unlock_period" + "unbond_config" ], "properties": { "allowed_lp_tokens": { @@ -1041,23 +1286,12 @@ "$ref": "#/definitions/Addr" } }, - "fee_tier_interval": { - "description": "This is the interval period in seconds on which we will have fee tier boundaries.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_fee_bp": { - "description": "Instant LP unbonding fee. This is the percentage of the LP tokens that will be deducted as fee value between 0 and 1000 (0% to 10%) are allowed", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "instant_unbond_min_fee_bp": { - "description": "This is the minimum fee charged for instant LP unlock when the unlock time is less than fee interval in future. Fee in between the unlock duration and fee tier intervals will be linearly interpolated at fee tier interval boundaries.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "allowed_reward_cw20_tokens": { + "description": "Allowed CW20 tokens for rewards. This is to control the abuse from a malicious CW20 token to create unnecessary reward schedules", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } }, "keeper": { "description": "Keeper address that acts as treasury of the Dexter protocol. All the fees are sent to this address.", @@ -1075,11 +1309,13 @@ } ] }, - "unlock_period": { - "description": "Unlocking period in seconds This is the minimum time that must pass before a user can withdraw their staked tokens and rewards after they have called the unbond function", - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "unbond_config": { + "description": "Default unbond config", + "allOf": [ + { + "$ref": "#/definitions/UnbondConfig" + } + ] } }, "additionalProperties": false, @@ -1087,6 +1323,78 @@ "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" + }, + "InstantUnbondConfig": { + "oneOf": [ + { + "type": "string", + "enum": [ + "disabled" + ] + }, + { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "object", + "required": [ + "fee_tier_interval", + "max_fee", + "min_fee" + ], + "properties": { + "fee_tier_interval": { + "description": "This is the interval period in seconds on which we will have fee tier boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "max_fee": { + "description": "Instant LP unbonding fee. This is the percentage of the LP tokens that will be deducted as fee value between 0 and 1000 (0% to 10%) are allowed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "min_fee": { + "description": "This is the minimum fee charged for instant LP unlock when the unlock time is less than fee interval in future. Fee in between the unlock duration and fee tier intervals will be linearly interpolated at fee tier interval boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "UnbondConfig": { + "type": "object", + "required": [ + "instant_unbond_config", + "unlock_period" + ], + "properties": { + "instant_unbond_config": { + "description": "Status of instant unbonding", + "allOf": [ + { + "$ref": "#/definitions/InstantUnbondConfig" + } + ] + }, + "unlock_period": { + "description": "Unlocking period in seconds This is the minimum time that must pass before a user can withdraw their staked tokens and rewards after they have called the unbond function", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -1120,6 +1428,42 @@ } } }, + "default_instant_unlock_fee_tiers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_UnlockFeeTier", + "type": "array", + "items": { + "$ref": "#/definitions/UnlockFeeTier" + }, + "definitions": { + "UnlockFeeTier": { + "type": "object", + "required": [ + "seconds_till_unlock_end", + "seconds_till_unlock_start", + "unlock_fee_bp" + ], + "properties": { + "seconds_till_unlock_end": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "seconds_till_unlock_start": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unlock_fee_bp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, "instant_unlock_fee": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantLpUnlockFee", @@ -1511,6 +1855,82 @@ } } }, + "unbond_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnbondConfig", + "type": "object", + "required": [ + "instant_unbond_config", + "unlock_period" + ], + "properties": { + "instant_unbond_config": { + "description": "Status of instant unbonding", + "allOf": [ + { + "$ref": "#/definitions/InstantUnbondConfig" + } + ] + }, + "unlock_period": { + "description": "Unlocking period in seconds This is the minimum time that must pass before a user can withdraw their staked tokens and rewards after they have called the unbond function", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "InstantUnbondConfig": { + "oneOf": [ + { + "type": "string", + "enum": [ + "disabled" + ] + }, + { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "object", + "required": [ + "fee_tier_interval", + "max_fee", + "min_fee" + ], + "properties": { + "fee_tier_interval": { + "description": "This is the interval period in seconds on which we will have fee tier boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "max_fee": { + "description": "Instant LP unbonding fee. This is the percentage of the LP tokens that will be deducted as fee value between 0 and 1000 (0% to 10%) are allowed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "min_fee": { + "description": "This is the minimum fee charged for instant LP unlock when the unlock time is less than fee interval in future. Fee in between the unlock duration and fee tier intervals will be linearly interpolated at fee tier interval boundaries.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, "unclaimed_rewards": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Array_of_UnclaimedReward", diff --git a/contracts/multi_staking/src/contract.rs b/contracts/multi_staking/src/contract.rs index 97fcb86..158e31b 100644 --- a/contracts/multi_staking/src/contract.rs +++ b/contracts/multi_staking/src/contract.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{ from_json, to_json_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Event, MessageInfo, Response, StdError, StdResult, Storage, Uint128, WasmMsg, }; -use std::{cmp::min, collections::HashMap}; +use std::collections::HashMap; use dexter::{ asset::AssetInfo, @@ -15,9 +15,9 @@ use dexter::{ propose_new_owner, }, multi_staking::{ - AssetRewardState, AssetStakerInfo, Config, ConfigV1, + AssetRewardState, AssetStakerInfo, Config, ConfigV1, ConfigV2_1, ConfigV2_2, ConfigV3, CreatorClaimableRewardState, Cw20HookMsg, ExecuteMsg, InstantLpUnlockFee, InstantiateMsg, - MigrateMsg, QueryMsg, RewardSchedule, TokenLockInfo, UnclaimedReward, ConfigV2_1, ConfigV2_2, + MigrateMsg, QueryMsg, RewardSchedule, TokenLockInfo, UnbondConfig, UnclaimedReward, }, }; @@ -26,16 +26,15 @@ use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw_storage_plus::Item; use dexter::asset::Asset; use dexter::helper::EventExt; -use dexter::multi_staking::{ - RewardScheduleResponse, MAX_ALLOWED_LP_TOKENS, MAX_INSTANT_UNBOND_FEE_BP, -}; +use dexter::multi_staking::{RewardScheduleResponse, MAX_ALLOWED_LP_TOKENS}; use crate::{ error::ContractError, state::{ next_reward_schedule_id, ASSET_LP_REWARD_STATE, ASSET_STAKER_INFO, CONFIG, - CREATOR_CLAIMABLE_REWARD, LP_GLOBAL_STATE, LP_TOKEN_ASSET_REWARD_SCHEDULE, - OWNERSHIP_PROPOSAL, REWARD_SCHEDULES, USER_BONDED_LP_TOKENS, USER_LP_TOKEN_LOCKS, + CREATOR_CLAIMABLE_REWARD, LP_GLOBAL_STATE, LP_OVERRIDE_CONFIG, + LP_TOKEN_ASSET_REWARD_SCHEDULE, OWNERSHIP_PROPOSAL, REWARD_SCHEDULES, + USER_BONDED_LP_TOKENS, USER_LP_TOKEN_LOCKS, }, }; use crate::{ @@ -54,6 +53,8 @@ const CONTRACT_VERSION_V1: &str = "1.0.0"; const CONTRACT_VERSION_V2: &str = "2.0.0"; const CONTRACT_VERSION_V2_1: &str = "2.1.0"; const CONTRACT_VERSION_V2_2: &str = "2.2.0"; +const CONTRACT_VERSION_V3: &str = "3.0.0"; + /// Contract version that is used for migration. const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -68,50 +69,28 @@ pub fn instantiate( ) -> ContractResult { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - if msg.instant_unbond_fee_bp > MAX_INSTANT_UNBOND_FEE_BP { - return Err(ContractError::InvalidInstantUnbondFee { - max_allowed: MAX_INSTANT_UNBOND_FEE_BP, - received: msg.instant_unbond_fee_bp, - }); - } - - if msg.instant_unbond_min_fee_bp > msg.instant_unbond_fee_bp { - return Err(ContractError::InvalidInstantUnbondMinFee { - max_allowed: msg.instant_unbond_fee_bp, - received: msg.instant_unbond_min_fee_bp, - }); - } - - if msg.fee_tier_interval > msg.unlock_period { - return Err(ContractError::InvalidFeeTierInterval { - max_allowed: msg.unlock_period, - received: msg.fee_tier_interval, - }); - } - // validate keeper address deps.api.addr_validate(&msg.keeper_addr.to_string())?; + msg.unbond_config.validate()?; CONFIG.save( deps.storage, &Config { - keeper: msg.keeper_addr, - unlock_period: msg.unlock_period, + keeper: msg.keeper_addr.clone(), owner: deps.api.addr_validate(msg.owner.as_str())?, allowed_lp_tokens: vec![], - instant_unbond_fee_bp: msg.instant_unbond_fee_bp, - instant_unbond_min_fee_bp: msg.instant_unbond_min_fee_bp, - fee_tier_interval: msg.fee_tier_interval, + unbond_config: msg.unbond_config.clone(), + allowed_reward_cw20_tokens: vec![], }, )?; Ok(Response::new().add_event( Event::from_info(concatcp!(CONTRACT_NAME, "::instantiate"), &info) .add_attribute("owner", msg.owner.to_string()) - .add_attribute("unlock_period", msg.unlock_period.to_string()) + .add_attribute("keeper", msg.keeper_addr.to_string()) .add_attribute( - "minimum_reward_schedule_proposal_start_delay", - msg.minimum_reward_schedule_proposal_start_delay.to_string(), + "unbond_config", + serde_json_wasm::to_string(&msg.unbond_config).unwrap(), ), )) } @@ -126,20 +105,15 @@ pub fn execute( match msg { ExecuteMsg::UpdateConfig { keeper_addr, - unlock_period, - instant_unbond_fee_bp, - instant_unbond_min_fee_bp, - fee_tier_interval, - } => update_config( - deps, - env, - info, - keeper_addr, - unlock_period, - instant_unbond_fee_bp, - instant_unbond_min_fee_bp, - fee_tier_interval, - ), + unbond_config, + } => update_config(deps, env, info, keeper_addr, unbond_config), + ExecuteMsg::SetCustomUnbondConfig { + lp_token, + unbond_config, + } => set_custom_unbond_config(deps, env, info, lp_token, unbond_config), + ExecuteMsg::UnsetCustomUnbondConfig { lp_token } => { + unset_custom_unbond_config(deps, env, info, lp_token) + } ExecuteMsg::AllowLpToken { lp_token } => allow_lp_token(deps, env, info, lp_token), ExecuteMsg::RemoveLpToken { lp_token } => remove_lp_token(deps, info, &lp_token), ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), @@ -170,10 +144,13 @@ pub fn execute( None => info.sender.clone(), }; + let sender = info.sender.clone(); + create_reward_schedule( deps, env, info, + sender, lp_token, title, start_block_time, @@ -211,6 +188,43 @@ pub fn execute( ExecuteMsg::ClaimUnallocatedReward { reward_schedule_id } => { claim_unallocated_reward(deps, env, info, reward_schedule_id) } + ExecuteMsg::AllowRewardCw20Token { addr } => { + let mut config = CONFIG.load(deps.storage)?; + // Verify that the message sender is the owner + if info.sender != config.owner { + return Err(ContractError::Unauthorized); + } + if config.allowed_reward_cw20_tokens.contains(&addr) { + return Err(ContractError::Cw20TokenAlreadyAllowed); + } + config + .allowed_reward_cw20_tokens + .push(deps.api.addr_validate(&addr.to_string())?); + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_event( + Event::from_info(concatcp!(CONTRACT_NAME, "::allow_reward_cw20_token"), &info) + .add_attribute("cw20_token", addr.to_string()), + )) + } + ExecuteMsg::RemoveRewardCw20Token { addr } => { + let mut config = CONFIG.load(deps.storage)?; + // Verify that the message sender is the owner + if info.sender != config.owner { + return Err(ContractError::Unauthorized); + } + + config.allowed_reward_cw20_tokens.retain(|x| x != &addr); + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_event( + Event::from_info( + concatcp!(CONTRACT_NAME, "::remove_reward_cw20_token"), + &info, + ) + .add_attribute("cw20_token", addr.to_string()), + )) + } ExecuteMsg::ProposeNewOwner { owner, expires_in } => { let config = CONFIG.load(deps.storage)?; let response = propose_new_owner( @@ -255,10 +269,7 @@ fn update_config( _env: Env, info: MessageInfo, keeper_addr: Option, - unlock_period: Option, - instant_unbond_fee_bp: Option, - instant_unbond_min_fee_bp: Option, - fee_tier_interval: Option, + unbond_config: Option, ) -> ContractResult { let mut config: Config = CONFIG.load(deps.storage)?; @@ -270,76 +281,77 @@ fn update_config( let mut event = Event::from_info(concatcp!(CONTRACT_NAME, "::update_config"), &info); if let Some(keeper_addr) = keeper_addr { + // validate + deps.api.addr_validate(&keeper_addr.to_string())?; config.keeper = keeper_addr.clone(); event = event.add_attribute("keeper_addr", keeper_addr.to_string()); } - if let Some(unlock_period) = unlock_period { - // validate if unlock period is greater than the fee tier interval, then reset the fee tier interval to unlock period as well - if fee_tier_interval.is_some() && fee_tier_interval.unwrap() > unlock_period { - return Err(ContractError::InvalidFeeTierInterval { - max_allowed: unlock_period, - received: fee_tier_interval.unwrap(), - }); - } - - // reset the current fee tier interval to unlock period if it is greater than unlock period - if config.fee_tier_interval > unlock_period { - config.fee_tier_interval = unlock_period; - event = event.add_attribute("fee_tier_interval", config.fee_tier_interval.to_string()); - } - - config.unlock_period = unlock_period; - event = event.add_attribute("unlock_period", config.unlock_period.to_string()); - } - - if let Some(instant_unbond_fee_bp) = instant_unbond_fee_bp { - // validate max allowed instant unbond fee which is 10% - if instant_unbond_fee_bp > MAX_INSTANT_UNBOND_FEE_BP { - return Err(ContractError::InvalidInstantUnbondFee { - max_allowed: MAX_INSTANT_UNBOND_FEE_BP, - received: instant_unbond_fee_bp, - }); - } - config.instant_unbond_fee_bp = instant_unbond_fee_bp; + if let Some(unbond_config) = unbond_config { + unbond_config.validate()?; event = event.add_attribute( - "instant_unbond_fee_bp", - config.instant_unbond_fee_bp.to_string(), + "unbond_config", + serde_json_wasm::to_string(&unbond_config).unwrap(), ); + config.unbond_config = unbond_config; } - if let Some(instant_unbond_min_fee_bp) = instant_unbond_min_fee_bp { - // validate min allowed instant unbond fee max value which is 10% and lesser than the instant unbond fee - if instant_unbond_min_fee_bp > MAX_INSTANT_UNBOND_FEE_BP - || instant_unbond_min_fee_bp > config.instant_unbond_fee_bp - { - return Err(ContractError::InvalidInstantUnbondMinFee { - max_allowed: min(config.instant_unbond_fee_bp, MAX_INSTANT_UNBOND_FEE_BP), - received: instant_unbond_min_fee_bp, - }); - } + CONFIG.save(deps.storage, &config)?; - config.instant_unbond_min_fee_bp = instant_unbond_min_fee_bp; - event = event.add_attribute( - "instant_unbond_min_fee_bp", - config.instant_unbond_min_fee_bp.to_string(), - ); + Ok(Response::new().add_event(event)) +} + +fn set_custom_unbond_config( + deps: DepsMut, + _env: Env, + info: MessageInfo, + lp_token: Addr, + unbond_config: UnbondConfig, +) -> ContractResult { + let config: Config = CONFIG.load(deps.storage)?; + + // Verify that the message sender is the owner + if info.sender != config.owner { + return Err(ContractError::Unauthorized); } - if let Some(fee_tier_interval) = fee_tier_interval { - // max allowed fee tier interval in equal to the unlock period. - if fee_tier_interval > config.unlock_period { - return Err(ContractError::InvalidFeeTierInterval { - max_allowed: config.unlock_period, - received: fee_tier_interval, - }); - } + let mut event = Event::from_info( + concatcp!(CONTRACT_NAME, "::set_custom_unbond_config"), + &info, + ); + + unbond_config.validate()?; + LP_OVERRIDE_CONFIG.save(deps.storage, lp_token.clone(), &unbond_config)?; - config.fee_tier_interval = fee_tier_interval; - event = event.add_attribute("fee_tier_interval", config.fee_tier_interval.to_string()); + event = event.add_attribute("lp_token", lp_token.to_string()); + event = event.add_attribute( + "unbond_config", + serde_json_wasm::to_string(&unbond_config).unwrap(), + ); + + Ok(Response::new().add_event(event)) +} + +fn unset_custom_unbond_config( + deps: DepsMut, + _env: Env, + info: MessageInfo, + lp_token: Addr, +) -> ContractResult { + let config: Config = CONFIG.load(deps.storage)?; + + // Verify that the message sender is the owner + if info.sender != config.owner { + return Err(ContractError::Unauthorized); } - CONFIG.save(deps.storage, &config)?; + let mut event = Event::from_info( + concatcp!(CONTRACT_NAME, "::unset_custom_unbond_config"), + &info, + ); + + LP_OVERRIDE_CONFIG.remove(deps.storage, lp_token.clone()); + event = event.add_attribute("lp_token", lp_token.to_string()); Ok(Response::new().add_event(event)) } @@ -525,7 +537,8 @@ fn remove_lp_token( pub fn create_reward_schedule( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, + sender: Addr, lp_token: Addr, title: String, start_block_time: u64, @@ -543,17 +556,13 @@ pub fn create_reward_schedule( end_block_time, }); } - if start_block_time <= env.block.time.seconds() - { + if start_block_time <= env.block.time.seconds() { return Err(ContractError::InvalidStartBlockTime { start_block_time, current_block_time: env.block.time.seconds(), }); } - // still need to check as an LP token might have been removed after the reward schedule was proposed - check_if_lp_token_allowed(&config, &lp_token)?; - let mut lp_global_state = LP_GLOBAL_STATE .may_load(deps.storage, &lp_token)? .unwrap_or_default(); @@ -594,7 +603,7 @@ pub fn create_reward_schedule( Ok(Response::new().add_event( Event::from_sender( concatcp!(CONTRACT_NAME, "::create_reward_schedule"), - &info.sender, + &sender, ) .add_attribute("creator", creator.to_string()) .add_attribute("lp_token", lp_token.to_string()) @@ -641,21 +650,28 @@ pub fn receive_cw20( } => { // only owner can create reward schedule let config = CONFIG.load(deps.storage)?; - if cw20_msg.sender != config.owner { + let sender = deps.api.addr_validate(&cw20_msg.sender)?; + if sender != config.owner { return Err(ContractError::Unauthorized); } let token_addr = info.sender.clone(); + // validate that the CW20 token is allowed for rewards + if !config.allowed_reward_cw20_tokens.contains(&token_addr) { + return Err(ContractError::Cw20TokenNotAllowed); + } + let creator = match actual_creator { Some(creator) => deps.api.addr_validate(&creator.to_string())?, - None => deps.api.addr_validate(&cw20_msg.sender)?, + None => sender.clone(), }; create_reward_schedule( deps, env, info, + sender, lp_token, title, start_block_time, @@ -986,6 +1002,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { token_lock, } => { let config = CONFIG.load(deps.storage)?; + let lp_override_config = LP_OVERRIDE_CONFIG.may_load(deps.storage, lp_token.clone())?; + + let unbond_config = lp_override_config.unwrap_or(config.unbond_config); + // validate if token lock actually exists let token_locks = USER_LP_TOKEN_LOCKS .may_load(deps.storage, (&lp_token, &user))? @@ -997,7 +1017,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { } let (fee_bp, unlock_fee) = - calculate_unlock_fee(&token_lock, env.block.time.seconds(), &config); + calculate_unlock_fee(&token_lock, env.block.time.seconds(), &unbond_config)?; let instant_lp_unlock_fee = InstantLpUnlockFee { time_until_lock_expiry: token_lock @@ -1011,21 +1031,20 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { to_json_binary(&instant_lp_unlock_fee).map_err(ContractError::from) } - QueryMsg::InstantUnlockFeeTiers {} => { + QueryMsg::InstantUnlockFeeTiers { lp_token } => { let config = CONFIG.load(deps.storage)?; - let min_fee = config.instant_unbond_min_fee_bp; - let max_fee = config.instant_unbond_fee_bp; + let lp_override_config = LP_OVERRIDE_CONFIG.may_load(deps.storage, lp_token)?; - let unlock_period = config.unlock_period; - let fee_tiers = query_instant_unlock_fee_tiers( - config.fee_tier_interval, - unlock_period, - min_fee, - max_fee, - ); + let fee_tiers = + query_instant_unlock_fee_tiers(lp_override_config.unwrap_or(config.unbond_config))?; to_json_binary(&fee_tiers).map_err(ContractError::from) } + QueryMsg::DefaultInstantUnlockFeeTiers {} => { + let config = CONFIG.load(deps.storage)?; + let fee_tiers = query_instant_unlock_fee_tiers(config.unbond_config)?; + to_json_binary(&fee_tiers).map_err(ContractError::from) + } QueryMsg::UnclaimedRewards { lp_token, user, @@ -1199,17 +1218,32 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { let config = CONFIG.load(deps.storage)?; to_json_binary(&config).map_err(ContractError::from) } + QueryMsg::UnbondConfig { lp_token } => { + let config = CONFIG.load(deps.storage)?; + + let unbond_config = if let Some(lp_token) = lp_token { + // validate address + let lp_token = deps.api.addr_validate(lp_token.as_str())?; + let lp_override_config = LP_OVERRIDE_CONFIG.may_load(deps.storage, lp_token)?; + + lp_override_config.unwrap_or(config.unbond_config) + } else { + config.unbond_config + }; + + to_json_binary(&unbond_config).map_err(ContractError::from) + } } } #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult { match msg { - MigrateMsg::V3FromV1 { + MigrateMsg::V3_1FromV1 { keeper_addr, instant_unbond_fee_bp, instant_unbond_min_fee_bp, - fee_tier_interval + fee_tier_interval, } => { // verify if we are running on V1 right now let contract_version = get_contract_version(deps.storage)?; @@ -1221,58 +1255,53 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult MAX_INSTANT_UNBOND_FEE_BP { - return Err(ContractError::InvalidInstantUnbondFee { - max_allowed: MAX_INSTANT_UNBOND_FEE_BP, - received: instant_unbond_fee_bp, - }); - } - - if instant_unbond_min_fee_bp > instant_unbond_fee_bp { - return Err(ContractError::InvalidInstantUnbondMinFee { - max_allowed: instant_unbond_fee_bp, - received: instant_unbond_min_fee_bp, - }); - } - let config_v1: ConfigV1 = Item::new("config").load(deps.storage)?; - // valiate fee tier interval - if fee_tier_interval > config_v1.unlock_period { - return Err(ContractError::InvalidFeeTierInterval { - max_allowed: config_v1.unlock_period, - received: fee_tier_interval, - }); - } + let unbond_config = UnbondConfig { + unlock_period: config_v1.unlock_period, + instant_unbond_config: dexter::multi_staking::InstantUnbondConfig::Enabled { + min_fee: instant_unbond_min_fee_bp, + max_fee: instant_unbond_fee_bp, + fee_tier_interval, + }, + }; + unbond_config.validate()?; // copy fields from v1 to v2 let config = Config { owner: config_v1.owner, allowed_lp_tokens: config_v1.allowed_lp_tokens, - unlock_period: config_v1.unlock_period, keeper: deps.api.addr_validate(&keeper_addr.to_string())?, - instant_unbond_fee_bp, - instant_unbond_min_fee_bp, - fee_tier_interval, + unbond_config, + allowed_reward_cw20_tokens: vec![], }; set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; CONFIG.save(deps.storage, &config)?; - }, - MigrateMsg::V3FromV2 { keeper_addr } => { + } + MigrateMsg::V3_1FromV2 { keeper_addr } => { let contract_version = get_contract_version(deps.storage)?; // if version is v2 or v2.1, apply the changes. - if contract_version.version == CONTRACT_VERSION_V2 || contract_version.version == CONTRACT_VERSION_V2_1 { + if contract_version.version == CONTRACT_VERSION_V2 + || contract_version.version == CONTRACT_VERSION_V2_1 + { let config_v2: ConfigV2_1 = Item::new("config").load(deps.storage)?; + let unbond_config = UnbondConfig { + unlock_period: config_v2.unlock_period, + instant_unbond_config: dexter::multi_staking::InstantUnbondConfig::Enabled { + min_fee: config_v2.instant_unbond_min_fee_bp, + max_fee: config_v2.instant_unbond_fee_bp, + fee_tier_interval: config_v2.fee_tier_interval, + }, + }; + unbond_config.validate()?; + let config = Config { owner: config_v2.owner, allowed_lp_tokens: config_v2.allowed_lp_tokens, - unlock_period: config_v2.unlock_period, - keeper: keeper_addr, - instant_unbond_fee_bp: config_v2.instant_unbond_fee_bp, - instant_unbond_min_fee_bp: config_v2.instant_unbond_min_fee_bp, - fee_tier_interval: config_v2.fee_tier_interval, + keeper: deps.api.addr_validate(&keeper_addr.to_string())?, + unbond_config, + allowed_reward_cw20_tokens: vec![], }; CONFIG.save(deps.storage, &config)?; @@ -1286,26 +1315,32 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult { + MigrateMsg::V3_1FromV2_2 {} => { let contract_version = get_contract_version(deps.storage)?; - // if version if v2.2 apply the changes and return if contract_version.version == CONTRACT_VERSION_V2_2 { let config_v2: ConfigV2_2 = Item::new("config").load(deps.storage)?; + let unbond_config = UnbondConfig { + unlock_period: config_v2.unlock_period, + instant_unbond_config: dexter::multi_staking::InstantUnbondConfig::Enabled { + min_fee: config_v2.instant_unbond_min_fee_bp, + max_fee: config_v2.instant_unbond_fee_bp, + fee_tier_interval: config_v2.fee_tier_interval, + }, + }; + unbond_config.validate()?; + let config = Config { owner: config_v2.owner, allowed_lp_tokens: config_v2.allowed_lp_tokens, - unlock_period: config_v2.unlock_period, keeper: config_v2.keeper, - instant_unbond_fee_bp: config_v2.instant_unbond_fee_bp, - instant_unbond_min_fee_bp: config_v2.instant_unbond_min_fee_bp, - fee_tier_interval: config_v2.fee_tier_interval, + unbond_config, + allowed_reward_cw20_tokens: vec![], }; CONFIG.save(deps.storage, &config)?; // set the contract version to v3 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - } else { return Err(ContractError::InvalidContractVersionForUpgrade { upgrade_version: CONTRACT_VERSION.to_string(), @@ -1314,6 +1349,41 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult { + let contract_version = get_contract_version(deps.storage)?; + if contract_version.version == CONTRACT_VERSION_V3 { + let config_v3: ConfigV3 = Item::new("config").load(deps.storage)?; + let unbond_config = UnbondConfig { + unlock_period: config_v3.unlock_period, + instant_unbond_config: dexter::multi_staking::InstantUnbondConfig::Enabled { + min_fee: config_v3.instant_unbond_min_fee_bp, + max_fee: config_v3.instant_unbond_fee_bp, + fee_tier_interval: config_v3.fee_tier_interval, + }, + }; + unbond_config.validate()?; + + let config = Config { + owner: config_v3.owner, + allowed_lp_tokens: config_v3.allowed_lp_tokens, + keeper: config_v3.keeper, + unbond_config, + allowed_reward_cw20_tokens: vec![], + }; + + CONFIG.save(deps.storage, &config)?; + + // set the contract version to v3 + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + } else { + return Err(ContractError::InvalidContractVersionForUpgrade { + upgrade_version: CONTRACT_VERSION.to_string(), + expected: CONTRACT_VERSION_V3.to_string(), + actual: contract_version.version, + }); + } + } } Ok(Response::default()) diff --git a/contracts/multi_staking/src/error.rs b/contracts/multi_staking/src/error.rs index c98f0af..3fd5777 100644 --- a/contracts/multi_staking/src/error.rs +++ b/contracts/multi_staking/src/error.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{OverflowError, StdError, Uint128}; +use cosmwasm_std::{CheckedMultiplyFractionError, OverflowError, StdError, Uint128}; +use dexter::multi_staking::UnbondConfigValidationError; use thiserror::Error; #[derive(Error, Debug)] @@ -6,6 +7,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("Checked multiply fraction error: {0}")] + CheckedMultiplyFractionError(CheckedMultiplyFractionError), + #[error("Unauthorized")] Unauthorized, @@ -96,15 +100,6 @@ pub enum ContractError { #[error("Token lock doesn't exist")] TokenLockNotFound, - #[error("Invalid instant unbond fee. Max allowed: {max_allowed}, Received: {received}")] - InvalidInstantUnbondFee { max_allowed: u64, received: u64 }, - - #[error("Invalid instant unbond min fee. Max allowed: {max_allowed}, Received: {received}")] - InvalidInstantUnbondMinFee { max_allowed: u64, received: u64 }, - - #[error("Invalid instant unlock fee tier interval. Max allowed: {max_allowed} i.e. equal to unlock period, Received: {received}")] - InvalidFeeTierInterval { max_allowed: u64, received: u64 }, - #[error("Invalid contract version for upgrade {upgrade_version}. Expected: {expected}, Actual: {actual}")] InvalidContractVersionForUpgrade { upgrade_version: String, @@ -117,6 +112,18 @@ pub enum ContractError { #[error("No valid lock found from supplied input which can be unlocked")] NoValidLocks, + + #[error("Instant unbond/unlock is disabled for this LP")] + InstantUnbondDisabled, + + #[error("Invalid unbond config. Error: {error}")] + InvalidUnbondConfig { error: UnbondConfigValidationError }, + + #[error("CW20 Token is already allowed as a reward asset")] + Cw20TokenAlreadyAllowed, + + #[error("This CW20 Token is not allowed as a reward asset")] + Cw20TokenNotAllowed, } impl From for ContractError { @@ -124,3 +131,9 @@ impl From for ContractError { StdError::from(o).into() } } + +impl From for ContractError { + fn from(error: UnbondConfigValidationError) -> Self { + ContractError::InvalidUnbondConfig { error } + } +} diff --git a/contracts/multi_staking/src/execute/unbond.rs b/contracts/multi_staking/src/execute/unbond.rs index c34eba3..db42434 100644 --- a/contracts/multi_staking/src/execute/unbond.rs +++ b/contracts/multi_staking/src/execute/unbond.rs @@ -1,14 +1,14 @@ use crate::{ contract::{update_staking_rewards, ContractResult, CONTRACT_NAME}, - state::USER_LP_TOKEN_LOCKS, + state::{LP_OVERRIDE_CONFIG, USER_LP_TOKEN_LOCKS}, }; use const_format::concatcp; -use cosmwasm_std::{Addr, DepsMut, Env, Event, MessageInfo, Response, Uint128}; +use cosmwasm_std::{Addr, Decimal, DepsMut, Env, Event, MessageInfo, Response, Uint128}; use dexter::{ asset::AssetInfo, helper::build_transfer_token_to_user_msg, - multi_staking::{Config, TokenLock, MAX_USER_LP_TOKEN_LOCKS}, + multi_staking::{Config, InstantUnbondConfig, TokenLock, MAX_USER_LP_TOKEN_LOCKS}, }; use dexter::helper::EventExt; @@ -40,6 +40,20 @@ pub fn instant_unbond( .unwrap_or_default(); let config: Config = CONFIG.load(deps.storage)?; + let lp_override_config = LP_OVERRIDE_CONFIG.may_load(deps.storage, lp_token.clone())?; + let unbond_config = lp_override_config.unwrap_or(config.unbond_config); + + // validate that ILPU is allowed + + let instant_unbond_fee_bp = match unbond_config.instant_unbond_config { + InstantUnbondConfig::Disabled => return Err(ContractError::InstantUnbondDisabled), + InstantUnbondConfig::Enabled { + min_fee: _, + max_fee, + fee_tier_interval: _, + } => max_fee, + }; + let mut lp_global_state = LP_GLOBAL_STATE.load(deps.storage, &lp_token)?; let user_updated_bond_amount = current_bond_amount.checked_sub(amount).map_err(|_| { @@ -74,7 +88,9 @@ pub fn instant_unbond( )?; // whole instant unbond fee is sent to the keeper as protocol treasury - let instant_unbond_fee = amount.multiply_ratio(config.instant_unbond_fee_bp, Uint128::from(10000u128)); + let instant_unbond_fee = amount + .checked_mul_ceil(Decimal::from_ratio(instant_unbond_fee_bp, 10000u64)) + .map_err(|err| ContractError::CheckedMultiplyFractionError(err))?; // Check if the keeper is available, if not, send the fee to the contract owner let fee_receiver = config.keeper; @@ -101,7 +117,10 @@ pub fn instant_unbond( .add_attribute("total_bond_amount", lp_global_state.total_bond_amount) .add_attribute("user_updated_bond_amount", user_updated_bond_amount) .add_attribute("instant_unbond_fee", instant_unbond_fee) - .add_attribute("user_withdrawn_amount", amount.checked_sub(instant_unbond_fee)?); + .add_attribute( + "user_withdrawn_amount", + amount.checked_sub(instant_unbond_fee)?, + ); response = response.add_event(event); Ok(response) @@ -172,8 +191,11 @@ pub fn unbond( } let config = CONFIG.load(deps.storage)?; + let override_unbond_config = LP_OVERRIDE_CONFIG.may_load(deps.storage, lp_token.clone())?; + + let unbond_config = override_unbond_config.unwrap_or(config.unbond_config); - let unlock_time = env.block.time.seconds() + config.unlock_period; + let unlock_time = env.block.time.seconds() + unbond_config.unlock_period; unlocks.push(TokenLock { unlock_time, amount, diff --git a/contracts/multi_staking/src/execute/unlock.rs b/contracts/multi_staking/src/execute/unlock.rs index 3b9e975..68b9f2a 100644 --- a/contracts/multi_staking/src/execute/unlock.rs +++ b/contracts/multi_staking/src/execute/unlock.rs @@ -1,7 +1,8 @@ use crate::{ contract::{ContractResult, CONTRACT_NAME}, - state::USER_LP_TOKEN_LOCKS, - utils::{calculate_unlock_fee, find_lock_difference}, error::ContractError, + error::ContractError, + state::{LP_OVERRIDE_CONFIG, USER_LP_TOKEN_LOCKS}, + utils::{calculate_unlock_fee, find_lock_difference}, }; use const_format::concatcp; use cosmwasm_std::{ @@ -26,6 +27,9 @@ pub fn instant_unlock( token_locks: Vec, ) -> ContractResult { let config = CONFIG.load(deps.storage)?; + let lp_override_config = LP_OVERRIDE_CONFIG.may_load(deps.storage, lp_token.clone())?; + let unbond_config = lp_override_config.unwrap_or(config.unbond_config); + let user = info.sender.clone(); let locks = USER_LP_TOKEN_LOCKS .may_load(deps.storage, (&lp_token, &user))? @@ -56,7 +60,7 @@ pub fn instant_unlock( let current_block_time = env.block.time.seconds(); for lock in valid_locks_to_be_unlocked.iter() { - let (_, unlock_fee) = calculate_unlock_fee(lock, current_block_time, &config); + let (_, unlock_fee) = calculate_unlock_fee(lock, current_block_time, &unbond_config)?; total_amount_to_be_unlocked += lock.amount.checked_sub(unlock_fee)?; total_fee_charged += unlock_fee; } diff --git a/contracts/multi_staking/src/query.rs b/contracts/multi_staking/src/query.rs index dc43ef9..537974b 100644 --- a/contracts/multi_staking/src/query.rs +++ b/contracts/multi_staking/src/query.rs @@ -1,56 +1,70 @@ use cosmwasm_std::Decimal; -use dexter::multi_staking::UnlockFeeTier; +use dexter::multi_staking::{InstantUnbondConfig, UnbondConfig, UnlockFeeTier}; + +use crate::error::ContractError; pub fn query_instant_unlock_fee_tiers( - tier_interval: u64, - unlock_period: u64, - min_fee_bp: u64, - max_fee_bp: u64, -) -> Vec { + config: UnbondConfig, +) -> Result, ContractError> { // Fee tiers exist on day boundaries linearly interpolating the values from min_fee to max_fee let mut fee_tiers: Vec = vec![]; - // if the unlock period is less than tier interval then there's only one tier equal to max fee - if unlock_period <= tier_interval { - fee_tiers.push(UnlockFeeTier { - seconds_till_unlock_end: unlock_period, - seconds_till_unlock_start: 0, - unlock_fee_bp: max_fee_bp, - }); - } else { - // num tiers is the ceiling of unlock period in terms of tier interval - let num_tiers = (Decimal::from_ratio(unlock_period, tier_interval)) - .to_uint_ceil() - .u128(); - // fee increment per tier - let fee_increment: Decimal = - Decimal::from_ratio(max_fee_bp - min_fee_bp, (num_tiers - 1) as u64); - - let mut tier_start_time = 0; - let mut tier_end_time = tier_interval; - - for tier in 0..num_tiers { - fee_tiers.push(UnlockFeeTier { - seconds_till_unlock_end: tier_end_time, - seconds_till_unlock_start: tier_start_time, - // unlock_fee_bp: min_fee + (fee_increment * tier) - unlock_fee_bp: min_fee_bp - + fee_increment - .checked_mul(Decimal::from_ratio(tier, 1u64)) - .unwrap() - .to_uint_ceil() - .u128() as u64, - }); - - tier_start_time = tier_end_time; - // if this is the last tier then set the end time to the unlock period - if tier == num_tiers - 2 { - tier_end_time = unlock_period; + let unlock_period = config.unlock_period; + + // if the ILPU is disabled, throw error saying that unlock is not supported + match config.instant_unbond_config { + InstantUnbondConfig::Disabled => { + // panic!("Instant unlock is not supported"); + Err(ContractError::InstantUnbondDisabled {}) + } + InstantUnbondConfig::Enabled { + min_fee, + max_fee, + fee_tier_interval, + } => { + // if the unlock period is less than tier interval then there's only one tier equal to max fee + if unlock_period <= fee_tier_interval { + fee_tiers.push(UnlockFeeTier { + seconds_till_unlock_end: unlock_period, + seconds_till_unlock_start: 0, + unlock_fee_bp: max_fee, + }); } else { - tier_end_time += tier_interval; + // num tiers is the ceiling of unlock period in terms of tier interval + let num_tiers = (Decimal::from_ratio(unlock_period, fee_tier_interval)) + .to_uint_ceil() + .u128(); + // fee increment per tier + let fee_increment: Decimal = + Decimal::from_ratio(max_fee - min_fee, (num_tiers - 1) as u64); + + let mut tier_start_time = 0; + let mut tier_end_time = fee_tier_interval; + + for tier in 0..num_tiers { + fee_tiers.push(UnlockFeeTier { + seconds_till_unlock_end: tier_end_time, + seconds_till_unlock_start: tier_start_time, + // unlock_fee_bp: min_fee + (fee_increment * tier) + unlock_fee_bp: min_fee + + fee_increment + .checked_mul(Decimal::from_ratio(tier, 1u64)) + .unwrap() + .to_uint_ceil() + .u128() as u64, + }); + + tier_start_time = tier_end_time; + // if this is the last tier then set the end time to the unlock period + if tier == num_tiers - 2 { + tier_end_time = unlock_period; + } else { + tier_end_time += fee_tier_interval; + } + } } + + Ok(fee_tiers) } } - - fee_tiers } diff --git a/contracts/multi_staking/src/state.rs b/contracts/multi_staking/src/state.rs index f8bc58c..b00cbb3 100644 --- a/contracts/multi_staking/src/state.rs +++ b/contracts/multi_staking/src/state.rs @@ -4,13 +4,16 @@ use dexter::{ helper::OwnershipProposal, multi_staking::{ AssetRewardState, AssetStakerInfo, Config, CreatorClaimableRewardState, LpGlobalState, - RewardSchedule, TokenLock, + RewardSchedule, TokenLock, UnbondConfig, }, }; // Global config of the contract pub const CONFIG: Item = Item::new("config"); +// LP can have custom overridden unbonding config +pub const LP_OVERRIDE_CONFIG: Map = Map::new("lp_override_config"); + /// Ownership proposal in case of ownership transfer is initiated pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); @@ -54,7 +57,6 @@ pub const ASSET_LP_REWARD_STATE: Map<(&str, &Addr), AssetRewardState> = /// being rewarded for the LP token. pub const LP_GLOBAL_STATE: Map<&Addr, LpGlobalState> = Map::new("lp_global_state"); - pub fn next_reward_schedule_id(store: &mut dyn Storage) -> StdResult { let id: u64 = REWARD_SCHEDULE_ID_COUNT .may_load(store)? diff --git a/contracts/multi_staking/src/utils.rs b/contracts/multi_staking/src/utils.rs index eef6604..b708ffb 100644 --- a/contracts/multi_staking/src/utils.rs +++ b/contracts/multi_staking/src/utils.rs @@ -1,7 +1,7 @@ -use cosmwasm_std::Uint128; -use dexter::multi_staking::{Config, TokenLock}; +use cosmwasm_std::{Decimal, Uint128}; +use dexter::multi_staking::{InstantUnbondConfig, TokenLock, UnbondConfig}; -use crate::query::query_instant_unlock_fee_tiers; +use crate::{error::ContractError, query::query_instant_unlock_fee_tiers}; /// Find the difference between two lock vectors. /// This must take into account that same looking lock can coexist, for example, there can be 2 locks for unlocking @@ -56,39 +56,43 @@ pub fn find_lock_difference( pub fn calculate_unlock_fee( token_lock: &TokenLock, current_block_time: u64, - config: &Config, -) -> (u64, Uint128) { + unbond_config: &UnbondConfig, +) -> Result<(u64, Uint128), ContractError> { let lock_end_time = token_lock.unlock_time; if current_block_time >= lock_end_time { - return (0, Uint128::zero()); + return Ok((0, Uint128::zero())); } - // This is the bounds of the fee calculation linear interpolation at tier_interval granularity. - let min_fee_bp = config.instant_unbond_min_fee_bp; - let max_fee_bp = config.instant_unbond_fee_bp; - - let tiers = query_instant_unlock_fee_tiers( - config.fee_tier_interval, - config.unlock_period, - min_fee_bp, - max_fee_bp, - ); - - // find applicable tier based on second left to unlock - let seconds_left_to_unlock = lock_end_time - current_block_time; - - let mut fee_bp = max_fee_bp; - for tier in tiers { - // the tier is applicable if the seconds fall in tiers range, end non-inclusive - if seconds_left_to_unlock >= tier.seconds_till_unlock_start - && seconds_left_to_unlock < tier.seconds_till_unlock_end - { - fee_bp = tier.unlock_fee_bp; - break; + // check if ILPU is enabled + match unbond_config.instant_unbond_config { + InstantUnbondConfig::Disabled => Err(ContractError::InstantUnbondDisabled {}), + InstantUnbondConfig::Enabled { + min_fee: _, + max_fee, + fee_tier_interval: _, + } => { + let tiers = query_instant_unlock_fee_tiers(unbond_config.clone())?; + + // find applicable tier based on second left to unlock + let seconds_left_to_unlock = lock_end_time - current_block_time; + + let mut fee_bp = max_fee; + for tier in tiers { + // the tier is applicable if the seconds fall in tiers range, end non-inclusive + if seconds_left_to_unlock >= tier.seconds_till_unlock_start + && seconds_left_to_unlock < tier.seconds_till_unlock_end + { + fee_bp = tier.unlock_fee_bp; + break; + } + } + + let fee = token_lock + .amount + .checked_mul_ceil(Decimal::from_ratio(fee_bp, 10000u64)) + .map_err(|err| ContractError::CheckedMultiplyFractionError(err))?; + Ok((fee_bp, fee)) } } - - let fee = token_lock.amount.multiply_ratio(fee_bp, 10000u64); - (fee_bp, fee) } diff --git a/contracts/multi_staking/tests/instant-unbonding.rs b/contracts/multi_staking/tests/instant-unbonding.rs index 4f75363..d98965d 100644 --- a/contracts/multi_staking/tests/instant-unbonding.rs +++ b/contracts/multi_staking/tests/instant-unbonding.rs @@ -1,12 +1,15 @@ use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; -use dexter::asset::AssetInfo; -use utils::update_fee_tier_interval; +use dexter::{ + asset::AssetInfo, + multi_staking::{InstantUnbondConfig, UnbondConfig}, +}; use crate::utils::{ assert_user_bonded_amount, assert_user_lp_token_balance, bond_lp_tokens, create_reward_schedule, instant_unbond_lp_tokens, instant_unlock_lp_tokens, - mint_lp_tokens_to_addr, mock_app, query_instant_lp_unlock_fee, query_instant_unlock_fee_tiers, - query_raw_token_locks, query_token_locks, setup_generic, unbond_lp_tokens, unlock_lp_tokens, + mint_lp_tokens_to_addr, mock_app, query_default_instant_unlock_fee_tiers, + query_instant_lp_unlock_fee, query_raw_token_locks, query_token_locks, setup_generic, + unbond_lp_tokens, unlock_lp_tokens, update_unbond_config, }; pub mod utils; @@ -29,7 +32,6 @@ fn validate_fee_tier_logic() { &mut app, admin_addr.clone(), keeper_addr.clone(), - 0, // 80 minutes less than 7 days. We should still have 7 tiers 600_000, 300, @@ -37,11 +39,11 @@ fn validate_fee_tier_logic() { 600_000, ); - // Update fee tier boundary to same time as unlock period - update_fee_tier_interval(&mut app, &admin_addr, &multi_staking_instance, 600_000).unwrap(); + // // Update fee tier boundary to same time as unlock period + // update_fee_tier_interval(&mut app, &admin_addr, &multi_staking_instance, 600_000).unwrap(); // query fee tiers - let fee_tiers = query_instant_unlock_fee_tiers(&mut app, &multi_staking_instance); + let fee_tiers = query_default_instant_unlock_fee_tiers(&mut app, &multi_staking_instance); // validate fee tiers. There should be 1 tier upto the unlock period boundary and max fee assert_eq!(fee_tiers.len(), 1); @@ -51,18 +53,43 @@ fn validate_fee_tier_logic() { // update fee tier boundary higher than unlock period to make sure we have 1 tier still // Added checks to make sure following condition is invalid for update - let result = update_fee_tier_interval(&mut app, &admin_addr, &multi_staking_instance, 600_001); + let result = update_unbond_config( + &mut app, + &admin_addr, + &multi_staking_instance, + UnbondConfig { + unlock_period: 600_000, + instant_unbond_config: InstantUnbondConfig::Enabled { + min_fee: 300, + max_fee: 500, + fee_tier_interval: 600_001, + }, + }, + ); assert!(result.is_err()); assert_eq!( result.unwrap_err().root_cause().to_string(), - "Invalid instant unlock fee tier interval. Max allowed: 600000 i.e. equal to unlock period, Received: 600001" + "Invalid unbond config. Error: Invalid fee tier interval. Fee tier interval must be a non-zero value lesser than the unlock period" ); // update fee tier boundary to 100_000 seconds and validate that we have 6 tiers which are equalled spaced - update_fee_tier_interval(&mut app, &admin_addr, &multi_staking_instance, 100_000).unwrap(); + update_unbond_config( + &mut app, + &admin_addr, + &multi_staking_instance, + UnbondConfig { + unlock_period: 600_000, + instant_unbond_config: InstantUnbondConfig::Enabled { + min_fee: 300, + max_fee: 500, + fee_tier_interval: 100_000, + }, + }, + ) + .unwrap(); // query fee tiers - let fee_tiers = query_instant_unlock_fee_tiers(&mut app, &multi_staking_instance); + let fee_tiers = query_default_instant_unlock_fee_tiers(&mut app, &multi_staking_instance); // validate fee tiers. There should be 6 tiers upto the unlock period boundary and max fee assert_eq!(fee_tiers.len(), 6); @@ -120,7 +147,6 @@ fn test_instant_unbond_and_unlock() { &mut app, admin_addr.clone(), keeper_addr.clone(), - 0, // 80 minutes less than 7 days. We should still have 7 tiers 600_000, 300, @@ -385,7 +411,7 @@ fn test_instant_unbond_and_unlock() { assert_eq!(token_lock_info[1].unlock_time, 1_001_502_400); // fetch current fee tiers for unlock - let fee_tiers = query_instant_unlock_fee_tiers(&mut app, &multi_staking_instance); + let fee_tiers = query_default_instant_unlock_fee_tiers(&mut app, &multi_staking_instance); // validate fee tiers assert_eq!(fee_tiers.len(), 7); diff --git a/contracts/multi_staking/tests/per_pool_unbonding_config.rs b/contracts/multi_staking/tests/per_pool_unbonding_config.rs new file mode 100644 index 0000000..0bd5ee9 --- /dev/null +++ b/contracts/multi_staking/tests/per_pool_unbonding_config.rs @@ -0,0 +1,555 @@ +use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; +use cw_multi_test::Executor; +use dexter::multi_staking::{ + ExecuteMsg, InstantLpUnlockFee, InstantUnbondConfig, QueryMsg, UnbondConfig, UnlockFeeTier, +}; +use utils::{ + instantiate_multi_staking_contract, store_lp_token_contract, store_multi_staking_contract, +}; + +use crate::utils::{ + assert_user_lp_token_balance, bond_lp_tokens, create_lp_token, instant_unbond_lp_tokens, + instant_unlock_lp_tokens, mint_lp_tokens_to_addr, mock_app, query_token_locks, + unbond_lp_tokens, +}; +pub mod utils; + +// This test performs the following steps: +// 1. Bonds some LP tokens for the user +// 2. Unbonds some of them normally creating a lock +// 3. Instatntly unbonds some of the tokens +// 4. Unbonds rest of the tokens normally creating a 2nd lock +// 4. Instatntly unlocks the tokens that were locked in step 2 paying the penalty fee +// 5. Validate if one of the lock still exists, the correct one and user balance is updated normally +// 6. Let the lock 2 expire and validate that user balance is updated normally post normal unlock operation. +#[test] +fn test_instant_unbond_and_unlock() { + let admin = String::from("admin"); + let keeper = String::from("keeper"); + let user = String::from("user"); + + let coins = vec![ + Coin::new(1000_000_000, "uxprt"), + Coin::new(1000_000_000, "uatom"), + ]; + + let admin_addr = Addr::unchecked(admin.clone()); + let user_addr = Addr::unchecked(user.clone()); + let keeper_addr = Addr::unchecked(keeper.clone()); + + let mut app = mock_app(admin_addr.clone(), coins); + + let multi_staking_code_id = store_multi_staking_contract(&mut app); + + let multi_staking_instance = instantiate_multi_staking_contract( + &mut app, + multi_staking_code_id, + admin_addr.clone(), + keeper_addr.clone(), + 600u64, + 200u64, + 500u64, + 240u64, + ); + + // let cw20_code_id = store_cw20_contract(app); + let lp_token_code_id = store_lp_token_contract(&mut app); + + let lp_token_addr_1 = create_lp_token( + &mut app, + lp_token_code_id, + admin_addr.clone(), + "Dummy LP Token".to_string(), + ); + + // Allow LP token in the multi staking contract + app.execute_contract( + admin_addr.clone(), + multi_staking_instance.clone(), + &ExecuteMsg::AllowLpToken { + lp_token: lp_token_addr_1.clone(), + }, + &vec![], + ) + .unwrap(); + + // let cw20_code_id = store_cw20_contract(app); + let lp_token_code_id = store_lp_token_contract(&mut app); + + let lp_token_addr_2 = create_lp_token( + &mut app, + lp_token_code_id, + admin_addr.clone(), + "Dummy LP Token".to_string(), + ); + + // Allow LP token in the multi staking contract for user + app.execute_contract( + admin_addr.clone(), + multi_staking_instance.clone(), + &ExecuteMsg::AllowLpToken { + lp_token: lp_token_addr_2.clone(), + }, + &vec![], + ) + .unwrap(); + + // disbale ILPU for lp token 2 and validate + let unbond_config = UnbondConfig { + unlock_period: 1000u64, + instant_unbond_config: InstantUnbondConfig::Disabled, + }; + + app.execute_contract( + admin_addr.clone(), + multi_staking_instance.clone(), + &ExecuteMsg::SetCustomUnbondConfig { + lp_token: lp_token_addr_2.clone(), + unbond_config: unbond_config.clone(), + }, + &vec![], + ) + .unwrap(); + + // query unbond config and validate + let query = QueryMsg::UnbondConfig { + lp_token: Some(lp_token_addr_2.clone()), + }; + let res: UnbondConfig = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!(res, unbond_config); + + // query fee tiers and validate that we get error in the query + let query = QueryMsg::InstantUnlockFeeTiers { + lp_token: lp_token_addr_2.clone(), + }; + let res: Result, _> = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query); + + // assert that we get error in the query + assert!(res.is_err()); + // assert error is that ILPU is disabled + assert_eq!( + res.err().unwrap().to_string(), + "Generic error: Querier contract error: Instant unbond/unlock is disabled for this LP" + ); + + // bond some LP tokens and validate that instant unbond fails + let user_bond_amount = Uint128::from(100000u128); + + // mint some LP tokens to user + mint_lp_tokens_to_addr( + &mut app, + &admin_addr, + &lp_token_addr_2, + &user_addr, + user_bond_amount, + ); + + bond_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + user_bond_amount, + ) + .unwrap(); + + // try to instant unbond and validate that it fails + let unbond_amount = Uint128::from(10000u128); + let res = instant_unbond_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + unbond_amount, + ); + + // assert that we get error in the response + assert!(res.is_err()); + // assert error is that ILPU is disabled + assert_eq!( + res.err().unwrap().root_cause().to_string(), + "Instant unbond/unlock is disabled for this LP" + ); + + // try to unbond normally and validate that it succeeds + let unbond_amount = Uint128::from(10000u128); + + unbond_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + unbond_amount, + ) + .unwrap(); + + // try to instant unlock some LP tokens and validate that it fails + let token_lock_info = query_token_locks( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + None, + ); + + let locks = token_lock_info.locks; + + let res = instant_unlock_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + // there's only one lock so we can use the full array + locks.clone(), + ); + + // assert that we get error in the response + assert!(res.is_err()); + // assert error is that ILPU is disabled + assert_eq!( + res.err().unwrap().root_cause().to_string(), + "Instant unbond/unlock is disabled for this LP" + ); + + // Add a custom unbonding config for LP token 2 + let unbond_config = UnbondConfig { + unlock_period: 1000u64, + instant_unbond_config: InstantUnbondConfig::Enabled { + min_fee: 200u64, + max_fee: 500u64, + fee_tier_interval: 300u64, + }, + }; + + app.execute_contract( + admin_addr.clone(), + multi_staking_instance.clone(), + &ExecuteMsg::SetCustomUnbondConfig { + lp_token: lp_token_addr_2.clone(), + unbond_config: unbond_config.clone(), + }, + &vec![], + ) + .unwrap(); + + // query fee tiers and validate + let query = QueryMsg::DefaultInstantUnlockFeeTiers {}; + let res: Vec = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + let default_fee_tiers = vec![ + UnlockFeeTier { + seconds_till_unlock_end: 240u64, + seconds_till_unlock_start: 0u64, + unlock_fee_bp: 200u64, + }, + UnlockFeeTier { + seconds_till_unlock_end: 480u64, + seconds_till_unlock_start: 240u64, + unlock_fee_bp: 350u64, + }, + UnlockFeeTier { + seconds_till_unlock_end: 600u64, + seconds_till_unlock_start: 480u64, + unlock_fee_bp: 500u64, + }, + ]; + assert_eq!(res, default_fee_tiers); + + // query fee tiers for the lp token 2 and validate + let query = QueryMsg::InstantUnlockFeeTiers { + lp_token: lp_token_addr_2.clone(), + }; + let res: Vec = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + let expected_fee_tiers = vec![ + UnlockFeeTier { + seconds_till_unlock_end: 300u64, + seconds_till_unlock_start: 0u64, + unlock_fee_bp: 200u64, + }, + UnlockFeeTier { + seconds_till_unlock_end: 600u64, + seconds_till_unlock_start: 300u64, + unlock_fee_bp: 300u64, + }, + UnlockFeeTier { + seconds_till_unlock_end: 900u64, + seconds_till_unlock_start: 600u64, + unlock_fee_bp: 400u64, + }, + UnlockFeeTier { + seconds_till_unlock_end: 1000u64, + seconds_till_unlock_start: 900u64, + unlock_fee_bp: 500u64, + }, + ]; + assert_eq!(res, expected_fee_tiers); + + // query fee tiers for the lp token 2 and validate + let query = QueryMsg::InstantUnlockFeeTiers { + lp_token: lp_token_addr_1.clone(), + }; + let res: Vec = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!(res, default_fee_tiers); + + // query instant unlock fee for lp token 2 and validate that we get a fee now and not an error + let query = QueryMsg::InstantUnlockFee { + lp_token: lp_token_addr_2.clone(), + token_lock: locks[0].clone(), + user: user_addr.clone(), + }; + + let res: InstantLpUnlockFee = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!( + res, + InstantLpUnlockFee { + unlock_fee: Uint128::from(500u128), + time_until_lock_expiry: 1000u64, + unlock_fee_bp: 500u64, + unlock_amount: Uint128::from(10000u128), + } + ); + + // increase the time by 201 seconds and validate that we moved to the correct tier + app.update_block(|b| { + b.time = Timestamp::from_seconds(1_000_000_201); + b.height = b.height + 200; + }); + + // query instant unlock fee for lp token 2 and validate that we get a fee now and not an error + let query = QueryMsg::InstantUnlockFee { + lp_token: lp_token_addr_2.clone(), + token_lock: locks[0].clone(), + user: user_addr.clone(), + }; + + let res: InstantLpUnlockFee = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!( + res, + InstantLpUnlockFee { + unlock_fee: Uint128::from(400u128), + time_until_lock_expiry: 799u64, + unlock_fee_bp: 400u64, + unlock_amount: Uint128::from(10000u128), + } + ); + + // perform instant unlock and validate that it succeeds + let res = instant_unlock_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + // there's only one lock so we can use the full array + locks.clone(), + ); + + // assert that we get no error in the response + assert!(res.is_ok()); + + // validate that the LP tokens after fee are transferred to the user + assert_user_lp_token_balance( + &mut app, + &user_addr, + &lp_token_addr_2, + Uint128::from(9600u128), + ); + + // validate that the fee is transferred to the keeper + assert_user_lp_token_balance( + &mut app, + &keeper_addr, + &lp_token_addr_2, + Uint128::from(400u128), + ); + + // let's add another token lock before we try the next experiment + unbond_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + Uint128::from(10000u128), + ) + .unwrap(); + + // create another one + unbond_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + Uint128::from(10000u128), + ) + .unwrap(); + + // query locks + let token_lock_info = query_token_locks( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + None, + ); + + let locks = token_lock_info.locks; + + // let's unset the custom unbond config and see what's the fee now, + // as the lock now would lie outside of the unlock_period range and thus out of every + // fee tier range. Ideally, the fee should be max fee and we should not get an error + + // unset the custom unbond config for LP token 2 + app.execute_contract( + admin_addr.clone(), + multi_staking_instance.clone(), + &ExecuteMsg::UnsetCustomUnbondConfig { + lp_token: lp_token_addr_2.clone(), + }, + &vec![], + ) + .unwrap(); + + // query new unlock fee tiers and validate that we get the default fee tiers + let query = QueryMsg::InstantUnlockFeeTiers { + lp_token: lp_token_addr_2.clone(), + }; + let res: Vec = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!(res, default_fee_tiers); + + // now let's query the unlock fee for our lock and validate that we get the max fee + let query = QueryMsg::InstantUnlockFee { + lp_token: lp_token_addr_2.clone(), + token_lock: locks[0].clone(), + user: user_addr.clone(), + }; + + let res: InstantLpUnlockFee = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!( + res, + InstantLpUnlockFee { + unlock_fee: Uint128::from(500u128), + time_until_lock_expiry: 1000u64, + unlock_fee_bp: 500u64, + unlock_amount: Uint128::from(10000u128), + } + ); + + // increase time to near the first tier boundary and validate same fee is being charged + app.update_block(|b| { + b.time = Timestamp::from_seconds(1_000_000_599); + b.height = b.height + 300; + }); + + let res: InstantLpUnlockFee = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!( + res, + InstantLpUnlockFee { + unlock_fee: Uint128::from(500u128), + time_until_lock_expiry: 602u64, + unlock_fee_bp: 500u64, + unlock_amount: Uint128::from(10000u128), + } + ); + + // now, at this time let's unlock the identical and lock and validate that + // 1. unlock succeeds + // 2. user balance is updated as expected + + // perform instant unlock and validate that it succeeds + let res = instant_unlock_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + // there's only one lock so we can use the full array + [locks[1].clone()].to_vec(), + ); + + // assert that we get no error in the response + assert!(res.is_ok()); + // validate that the LP tokens after fee are transferred to the user + assert_user_lp_token_balance( + &mut app, + &user_addr, + &lp_token_addr_2, + Uint128::from(19100u128), + ); + + // increase time to be just in the next tier and validate that we get the next tier fee + app.update_block(|b| { + b.time = Timestamp::from_seconds(1_000_000_722); + b.height = b.height + 2; + }); + + let res: InstantLpUnlockFee = app + .wrap() + .query_wasm_smart(multi_staking_instance.clone(), &query) + .unwrap(); + + assert_eq!( + res, + InstantLpUnlockFee { + unlock_fee: Uint128::from(350u128), + time_until_lock_expiry: 479u64, + unlock_fee_bp: 350u64, + unlock_amount: Uint128::from(10000u128), + } + ); + + // let's unlock the lock and validate fee is charged correctly + // perform instant unlock and validate that it succeeds + let res = instant_unlock_lp_tokens( + &mut app, + &multi_staking_instance, + &lp_token_addr_2, + &user_addr, + // there's only one lock so we can use the full array + [locks[0].clone()].to_vec(), + ); + + // assert that we get no error in the response + assert!(res.is_ok()); + // validate that the LP tokens after fee are transferred to the user + assert_user_lp_token_balance( + &mut app, + &user_addr, + &lp_token_addr_2, + Uint128::from(28750u128), + ); +} diff --git a/contracts/multi_staking/tests/staking.rs b/contracts/multi_staking/tests/staking.rs index 9031588..052044c 100644 --- a/contracts/multi_staking/tests/staking.rs +++ b/contracts/multi_staking/tests/staking.rs @@ -10,7 +10,7 @@ use crate::utils::{ create_reward_schedule, disallow_lp_token, mint_cw20_tokens_to_addr, mint_lp_tokens_to_addr, mock_app, query_balance, query_bonded_lp_tokens, query_cw20_balance, query_token_locks, query_unclaimed_rewards, setup, setup_generic, store_cw20_contract, unbond_lp_tokens, - unlock_lp_tokens, withdraw_unclaimed_rewards, + unlock_lp_tokens, whitelist_cw20_token_for_rewards, withdraw_unclaimed_rewards, }; pub mod utils; @@ -35,7 +35,6 @@ fn test_staking() { &mut app, admin_addr.clone(), keeper_addr, - 3 * 24 * 60 * 60, 1000, 200, 500, @@ -1022,6 +1021,31 @@ fn test_create_cw20_reward_schedule() { 1000_302_000, ); + assert!(result.is_err()); + + // whitelist cw20 token for rewards + whitelist_cw20_token_for_rewards( + &mut app, + &admin_addr, + &multi_staking_instance, + &cw20_token_addr, + ); + + // create again + let result = create_reward_schedule( + &mut app, + &admin_addr, + &multi_staking_instance, + &lp_token_addr, + "CW20 reward schedule".to_string(), + AssetInfo::Token { + contract_addr: cw20_token_addr.clone(), + }, + Uint128::from(100_000_000u64), + 1000_301_000, + 1000_302_000, + ); + assert!(result.is_ok()); // mint lp tokens to user 1 diff --git a/contracts/multi_staking/tests/utils/mod.rs b/contracts/multi_staking/tests/utils/mod.rs index f67d59c..6dae345 100644 --- a/contracts/multi_staking/tests/utils/mod.rs +++ b/contracts/multi_staking/tests/utils/mod.rs @@ -4,8 +4,8 @@ use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; use dexter::{ asset::AssetInfo, multi_staking::{ - Cw20HookMsg, ExecuteMsg, InstantLpUnlockFee, InstantiateMsg, QueryMsg, TokenLock, - TokenLockInfo, UnclaimedReward, UnlockFeeTier, + Cw20HookMsg, ExecuteMsg, InstantLpUnlockFee, InstantUnbondConfig, InstantiateMsg, QueryMsg, + TokenLock, TokenLockInfo, UnbondConfig, UnclaimedReward, UnlockFeeTier, }, }; @@ -28,7 +28,6 @@ pub fn instantiate_multi_staking_contract( code_id: u64, admin: Addr, keeper_addr: Addr, - minimum_reward_schedule_proposal_start_delay: u64, unlock_period: u64, instant_unbond_min_fee_bp: u64, instant_unbond_max_fee_bp: u64, @@ -36,13 +35,15 @@ pub fn instantiate_multi_staking_contract( ) -> Addr { let instantiate_msg = InstantiateMsg { owner: admin.clone(), - unlock_period, keeper_addr, - // 3 day delay - minimum_reward_schedule_proposal_start_delay, - instant_unbond_fee_bp: instant_unbond_max_fee_bp, - instant_unbond_min_fee_bp, - fee_tier_interval, + unbond_config: UnbondConfig { + unlock_period, + instant_unbond_config: InstantUnbondConfig::Enabled { + min_fee: instant_unbond_min_fee_bp, + max_fee: instant_unbond_max_fee_bp, + fee_tier_interval, + }, + }, }; let multi_staking_instance = app @@ -147,7 +148,6 @@ pub fn setup_generic( app: &mut App, admin_addr: Addr, keeper_addr: Addr, - minimum_reward_schedule_proposal_start_delay: u64, unlock_time: u64, unbond_fee_min_bp: u64, unbond_fee_max_bp: u64, @@ -159,7 +159,6 @@ pub fn setup_generic( multi_staking_code_id, admin_addr.clone(), keeper_addr, - minimum_reward_schedule_proposal_start_delay, unlock_time, unbond_fee_min_bp, unbond_fee_max_bp, @@ -196,7 +195,6 @@ pub fn setup(app: &mut App, admin_addr: Addr) -> (Addr, Addr) { app, admin_addr, keeper_addr, - 3 * 24 * 60 * 60, // 7 days 7 * 24 * 60 * 60, 200, @@ -268,21 +266,18 @@ pub fn create_reward_schedule( return reward_schedule_id; } -pub fn update_fee_tier_interval( +pub fn update_unbond_config( app: &mut App, admin_addr: &Addr, multistaking_contract: &Addr, - fee_tier_interval: u64, + unbond_config: UnbondConfig, ) -> anyhow::Result { app.execute_contract( admin_addr.clone(), multistaking_contract.clone(), &ExecuteMsg::UpdateConfig { keeper_addr: None, - unlock_period: None, - instant_unbond_fee_bp: None, - instant_unbond_min_fee_bp: None, - fee_tier_interval: Some(fee_tier_interval), + unbond_config: Some(unbond_config), }, &vec![], ) @@ -307,6 +302,23 @@ pub fn mint_lp_tokens_to_addr( .unwrap(); } +pub fn whitelist_cw20_token_for_rewards( + app: &mut App, + admin_addr: &Addr, + multistaking_contract: &Addr, + cw20_addr: &Addr, +) { + app.execute_contract( + admin_addr.clone(), + multistaking_contract.clone(), + &ExecuteMsg::AllowRewardCw20Token { + addr: cw20_addr.clone(), + }, + &vec![], + ) + .unwrap(); +} + pub fn mint_cw20_tokens_to_addr( app: &mut App, admin_addr: &Addr, @@ -590,6 +602,24 @@ pub fn query_balance(app: &mut App, user_addr: &Addr) -> Vec { } pub fn query_instant_unlock_fee_tiers( + app: &mut App, + lp_token: &Addr, + multistaking_contract: &Addr, +) -> Vec { + let fee_tiers: Vec = app + .wrap() + .query_wasm_smart( + multistaking_contract.clone(), + &QueryMsg::InstantUnlockFeeTiers { + lp_token: lp_token.clone(), + }, + ) + .unwrap(); + + return fee_tiers; +} + +pub fn query_default_instant_unlock_fee_tiers( app: &mut App, multistaking_contract: &Addr, ) -> Vec { @@ -597,7 +627,7 @@ pub fn query_instant_unlock_fee_tiers( .wrap() .query_wasm_smart( multistaking_contract.clone(), - &QueryMsg::InstantUnlockFeeTiers {}, + &QueryMsg::DefaultInstantUnlockFeeTiers {}, ) .unwrap(); diff --git a/contracts/superfluid_lp/tests/integration.rs b/contracts/superfluid_lp/tests/integration.rs index 8b4be64..9aaef06 100644 --- a/contracts/superfluid_lp/tests/integration.rs +++ b/contracts/superfluid_lp/tests/integration.rs @@ -3,6 +3,7 @@ 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::multi_staking::UnbondConfig; use dexter::vault::{FeeInfo, PauseInfo, PoolCreationFee, PoolTypeConfig, NativeAssetPrecisionInfo}; const EPOCH_START: u64 = 1_000_000; @@ -118,12 +119,15 @@ fn instantiate_contract(app: &mut App, owner: &Addr) -> (Addr, Addr, Addr) { // 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, + unbond_config: UnbondConfig { + unlock_period: 1000, + instant_unbond_config: dexter::multi_staking::InstantUnbondConfig::Enabled { + min_fee: 200, + max_fee: 500, + fee_tier_interval: 1000 + } + }, }; let multi_staking_instance = app diff --git a/contracts/vault/tests/utils/mod.rs b/contracts/vault/tests/utils/mod.rs index 1a7e1a4..bf10584 100644 --- a/contracts/vault/tests/utils/mod.rs +++ b/contracts/vault/tests/utils/mod.rs @@ -137,12 +137,15 @@ pub fn initialize_multistaking_contract( let multistaking_init_msg = dexter::multi_staking::InstantiateMsg { owner: owner.clone(), - unlock_period: 86400u64, keeper_addr: keeper.clone(), - minimum_reward_schedule_proposal_start_delay: 3 * 24 * 60 * 60, - instant_unbond_fee_bp: 500u64, - instant_unbond_min_fee_bp: 200u64, - fee_tier_interval: 86400u64, + unbond_config: dexter::multi_staking::UnbondConfig { + instant_unbond_config: dexter::multi_staking::InstantUnbondConfig::Enabled { + min_fee: 200u64, + max_fee: 500u64, + fee_tier_interval: 86400u64, + }, + unlock_period: 86400u64, + }, }; let multistaking_instance = app diff --git a/packages/dexter/src/multi_staking.rs b/packages/dexter/src/multi_staking.rs index e195c28..7213cd7 100644 --- a/packages/dexter/src/multi_staking.rs +++ b/packages/dexter/src/multi_staking.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Decimal, Uint128}; use cw20::Cw20ReceiveMsg; +use thiserror::Error; use crate::asset::AssetInfo; @@ -19,31 +20,28 @@ pub const MAX_INSTANT_UNBOND_FEE_BP: u64 = 1000; #[cw_serde] pub struct InstantiateMsg { pub owner: Addr, - pub unlock_period: u64, - pub minimum_reward_schedule_proposal_start_delay: u64, pub keeper_addr: Addr, - /// value between 0 and 1000 (0% to 10%) are allowed - pub instant_unbond_fee_bp: u64, - pub instant_unbond_min_fee_bp: u64, - pub fee_tier_interval: u64 + pub unbond_config: UnbondConfig, } #[cw_serde] pub enum MigrateMsg { - // Removes the reward schedule proposal start delay config param. - // This migration is supported from version v2.0, v2.1 and v2.2 - V3FromV2 { - keeper_addr: Addr - }, - V3FromV2_2 {}, + /// Removes the reward schedule proposal start delay config param /// Instant unbonding fee and keeper address are added - V3FromV1 { + V3_1FromV1 { keeper_addr: Addr, instant_unbond_fee_bp: u64, instant_unbond_min_fee_bp: u64, fee_tier_interval: u64 - } + }, + // Removes the reward schedule proposal start delay config param. + // This migration is supported from version v2.0, v2.1 and v2.2 + V3_1FromV2 { + keeper_addr: Addr + }, + V3_1FromV2_2 {}, + V3_1FromV3 {}, } #[cw_serde] @@ -107,18 +105,76 @@ pub struct Config { pub keeper: Addr, /// LP Token addresses for which reward schedules can be added pub allowed_lp_tokens: Vec, + /// Allowed CW20 tokens for rewards. This is to control the abuse from a malicious CW20 token to create + /// unnecessary reward schedules + pub allowed_reward_cw20_tokens: Vec, + /// Default unbond config + pub unbond_config: UnbondConfig +} + +#[cw_serde] +pub enum InstantUnbondConfig { + Disabled, + Enabled { + /// This is the minimum fee charged for instant LP unlock when the unlock time is less than fee interval in future. + /// Fee in between the unlock duration and fee tier intervals will be linearly interpolated at fee tier interval boundaries. + min_fee: u64, + /// Instant LP unbonding fee. This is the percentage of the LP tokens that will be deducted as fee + /// value between 0 and 1000 (0% to 10%) are allowed + max_fee: u64, + /// This is the interval period in seconds on which we will have fee tier boundaries. + fee_tier_interval: u64, + } +} + +#[cw_serde] +pub struct UnbondConfig { /// Unlocking period in seconds /// This is the minimum time that must pass before a user can withdraw their staked tokens and rewards /// after they have called the unbond function pub unlock_period: u64, - /// Instant LP unbonding fee. This is the percentage of the LP tokens that will be deducted as fee - /// value between 0 and 1000 (0% to 10%) are allowed - pub instant_unbond_fee_bp: u64, - /// This is the interval period in seconds on which we will have fee tier boundaries. - pub fee_tier_interval: u64, - /// This is the minimum fee charged for instant LP unlock when the unlock time is less than fee interval in future. - /// Fee in between the unlock duration and fee tier intervals will be linearly interpolated at fee tier interval boundaries. - pub instant_unbond_min_fee_bp: u64, + /// Status of instant unbonding + pub instant_unbond_config: InstantUnbondConfig +} + +#[derive(Error, Debug, PartialEq)] +pub enum UnbondConfigValidationError { + + #[error("Min fee smaller than max fee is not allowed")] + InvalidMinFee { min_fee: u64, max_fee: u64 }, + + #[error("Max fee bigger than {MAX_INSTANT_UNBOND_FEE_BP} is not allowed")] + InvalidMaxFee { max_fee: u64 }, + + #[error("Invalid fee tier interval. Fee tier interval must be a non-zero value lesser than the unlock period")] + InvalidFeeTierInterval { fee_tier_interval: u64 }, +} + +impl UnbondConfig { + // validate the unbond config + pub fn validate(&self) -> Result<(), UnbondConfigValidationError> { + match self.instant_unbond_config { + InstantUnbondConfig::Disabled => Ok(()), + InstantUnbondConfig::Enabled { min_fee, max_fee, fee_tier_interval } => { + if min_fee > max_fee { + Err(UnbondConfigValidationError::InvalidMinFee { + min_fee, + max_fee, + }) + } else if max_fee > MAX_INSTANT_UNBOND_FEE_BP { + Err(UnbondConfigValidationError::InvalidMaxFee { + max_fee + }) + } else if fee_tier_interval == 0 || fee_tier_interval > self.unlock_period { + Err(UnbondConfigValidationError::InvalidFeeTierInterval { + fee_tier_interval, + }) + } else { + Ok(()) + } + } + } + } } /// config structure of contract version v2 and v2.1 . Used for migration. @@ -147,6 +203,18 @@ pub struct ConfigV2_2 { pub instant_unbond_min_fee_bp: u64, } +/// config structure of contract version v2.2 . Used for migration. +#[cw_serde] +pub struct ConfigV3 { + pub owner: Addr, + pub keeper: Addr, + pub allowed_lp_tokens: Vec, + pub unlock_period: u64, + pub instant_unbond_fee_bp: u64, + pub fee_tier_interval: u64, + pub instant_unbond_min_fee_bp: u64, +} + /// config structure of contract version v1. Used for migration. #[cw_serde] pub struct ConfigV1 { @@ -204,6 +272,11 @@ pub enum QueryMsg { /// Returns current config of the contract #[returns(Config)] Config {}, + /// Returns current unbond config of a given LP token (or global) + #[returns(UnbondConfig)] + UnbondConfig { + lp_token: Option + }, /// Returns currently unclaimed rewards for a user for a give LP token /// If a future block time is provided, it will return the unclaimed rewards till that block time. #[returns(Vec)] @@ -243,7 +316,11 @@ pub enum QueryMsg { token_lock: TokenLock }, #[returns(Vec)] - InstantUnlockFeeTiers {}, + InstantUnlockFeeTiers { + lp_token: Addr + }, + #[returns(Vec)] + DefaultInstantUnlockFeeTiers {}, /// Returns the LP tokens which are whitelisted for rewards #[returns(Vec)] AllowedLPTokensForReward {}, @@ -291,10 +368,17 @@ pub enum ExecuteMsg { /// Allows an admin to update config params UpdateConfig { keeper_addr: Option, - unlock_period: Option, - instant_unbond_fee_bp: Option, - instant_unbond_min_fee_bp: Option, - fee_tier_interval: Option, + unbond_config: Option, + }, + /// Add custom unbdond config for a given LP token + SetCustomUnbondConfig { + lp_token: Addr, + unbond_config: UnbondConfig, + }, + + /// Unset custom unbdond config for a given LP token + UnsetCustomUnbondConfig { + lp_token: Addr, }, /// Creates a new reward schedule for rewarding LP token holders a specific asset. /// Asset is distributed linearly over the duration of the reward schedule. @@ -321,6 +405,14 @@ pub enum ExecuteMsg { RemoveLpToken { lp_token: Addr, }, + /// Add reward CW20 token to the list of allowed reward tokens + AllowRewardCw20Token { + addr: Addr + }, + /// Remove reward CW20 token from the list of allowed reward tokens + RemoveRewardCw20Token { + addr: Addr, + }, /// Allows the contract to receive CW20 tokens. /// The contract can receive CW20 tokens from LP tokens for staking and /// CW20 assets to be used as rewards.