diff --git a/Cargo.lock b/Cargo.lock index 838e1953..5d6c5951 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-ownable", "cw-storage-plus", "cw-utils", "cw2", diff --git a/contracts/liquidity_hub/bonding-manager/Cargo.toml b/contracts/liquidity_hub/bonding-manager/Cargo.toml index 5e913cf3..a00aaa01 100644 --- a/contracts/liquidity_hub/bonding-manager/Cargo.toml +++ b/contracts/liquidity_hub/bonding-manager/Cargo.toml @@ -40,6 +40,7 @@ thiserror.workspace = true white-whale-std.workspace = true cw-utils.workspace = true pool-manager.workspace = true +cw-ownable.workspace = true [dev-dependencies] cw-multi-test.workspace = true diff --git a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json index 63d83d38..ff973b1a 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json +++ b/contracts/liquidity_hub/bonding-manager/schema/bonding-manager.json @@ -9,6 +9,7 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", "unbonding_period" @@ -25,6 +26,10 @@ "description": "Denom to be swapped to and rewarded", "type": "string" }, + "epoch_manager_addr": { + "description": "The epoch manager contract", + "type": "string" + }, "grace_period": { "description": "Grace period the maximum age of a bucket before fees are forwarded from it", "allOf": [ @@ -42,7 +47,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" @@ -68,17 +73,10 @@ "oneOf": [ { "description": "Bonds the specified [Asset].", - "type": "object", - "required": [ + "type": "string", + "enum": [ "bond" - ], - "properties": { - "bond": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Unbonds the specified [Asset].", @@ -94,7 +92,12 @@ ], "properties": { "asset": { - "$ref": "#/definitions/Coin" + "description": "The asset to unbond.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] } }, "additionalProperties": false @@ -116,6 +119,7 @@ ], "properties": { "denom": { + "description": "The denom to withdraw.", "type": "string" } }, @@ -135,6 +139,7 @@ "type": "object", "properties": { "growth_rate": { + "description": "The new growth rate.", "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -144,19 +149,15 @@ } ] }, - "owner": { - "type": [ - "string", - "null" - ] - }, "pool_manager_addr": { + "description": "The new pool manager address.", "type": [ "string", "null" ] }, "unbonding_period": { + "description": "The unbonding period.", "anyOf": [ { "$ref": "#/definitions/Uint64" @@ -173,27 +174,21 @@ "additionalProperties": false }, { - "type": "object", - "required": [ + "description": "Claims the available rewards", + "type": "string", + "enum": [ "claim" - ], - "properties": { - "claim": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Fills the whale lair with new rewards.", + "description": "Fills the contract with new rewards.", "type": "string", "enum": [ "fill_rewards" ] }, { - "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", + "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", "type": "object", "required": [ "epoch_changed_hook" @@ -206,16 +201,85 @@ ], "properties": { "current_epoch": { - "$ref": "#/definitions/Epoch" + "description": "The current epoch, the one that was newly created.", + "allOf": [ + { + "$ref": "#/definitions/Epoch" + } + ] } }, "additionalProperties": false } }, "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false } ], "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, "Coin": { "type": "object", "required": [ @@ -253,6 +317,53 @@ }, "additionalProperties": false }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ @@ -277,17 +388,10 @@ "oneOf": [ { "description": "Returns the [Config] of te contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Returns the amount of assets that have been bonded by the specified address.", @@ -298,12 +402,13 @@ "properties": { "bonded": { "type": "object", - "required": [ - "address" - ], "properties": { "address": { - "type": "string" + "description": "The address to check for bonded assets. If none is provided, all bonded assets in the contract are returned.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false @@ -326,12 +431,15 @@ ], "properties": { "address": { + "description": "The address to check for unbonding assets.", "type": "string" }, "denom": { + "description": "The denom to check for unbonding assets.", "type": "string" }, "limit": { + "description": "The maximum amount of unbonding assets to return.", "type": [ "integer", "null" @@ -340,6 +448,7 @@ "minimum": 0.0 }, "start_after": { + "description": "The amount of unbonding assets to skip. Allows pagination.", "type": [ "integer", "null" @@ -368,9 +477,11 @@ ], "properties": { "address": { + "description": "The address to check for withdrawable assets.", "type": "string" }, "denom": { + "description": "The denom to check for withdrawable assets.", "type": "string" } }, @@ -393,9 +504,11 @@ ], "properties": { "address": { + "description": "The address to check for weight.", "type": "string" }, "global_index": { + "description": "The global index to check for weight. If none is provided, the current global index is used.", "anyOf": [ { "$ref": "#/definitions/GlobalIndex" @@ -406,6 +519,7 @@ ] }, "timestamp": { + "description": "The timestamp to check for weight. If none is provided, the current block time is used.", "anyOf": [ { "$ref": "#/definitions/Timestamp" @@ -423,63 +537,50 @@ }, { "description": "Returns the total amount of assets that have been bonded to the contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "total_bonded" - ], - "properties": { - "total_bonded": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Returns the global index of the contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "global_index" - ], - "properties": { - "global_index": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Returns the [Epoch]s that can be claimed.", + "description": "Returns the [Epoch]s that can be claimed by an address.", "type": "object", "required": [ - "claimable_epochs" + "claimable" ], "properties": { - "claimable_epochs": { + "claimable": { "type": "object", + "properties": { + "address": { + "description": "The address to check for claimable epochs. If none is provided, all possible epochs stored in the contract that can potentially be claimed are returned.", + "type": [ + "string", + "null" + ] + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Returns the [Epoch]s that can be claimed by an address.", + "description": "Query the contract's ownership information", "type": "object", "required": [ - "claimable" + "ownership" ], "properties": { - "claimable": { + "ownership": { "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "type": "string" - } - }, "additionalProperties": false } }, @@ -578,63 +679,34 @@ "type": "object", "required": [ "bonded_assets", - "first_bonded_epoch_id", "total_bonded" ], "properties": { "bonded_assets": { + "description": "The total amount of bonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] }, "total_bonded": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { + "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", + "allOf": [ + { "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" } - } - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "claimable": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ClaimableEpochsResponse", - "type": "object", - "required": [ - "epochs" - ], - "properties": { - "epochs": { - "type": "array", - "items": { - "$ref": "#/definitions/Epoch" - } + ] } }, "additionalProperties": false, @@ -654,98 +726,6 @@ } } }, - "Epoch": { - "type": "object", - "required": [ - "available", - "claimed", - "global_index", - "id", - "start_time", - "total" - ], - "properties": { - "available": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "claimed": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "global_index": { - "$ref": "#/definitions/GlobalIndex" - }, - "id": { - "$ref": "#/definitions/Uint64" - }, - "start_time": { - "$ref": "#/definitions/Timestamp" - }, - "total": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - }, - "additionalProperties": false - }, - "GlobalIndex": { - "type": "object", - "required": [ - "bonded_amount", - "bonded_assets", - "timestamp", - "weight" - ], - "properties": { - "bonded_amount": { - "description": "The total amount of tokens bonded in the contract.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "bonded_assets": { - "description": "Assets that are bonded in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "timestamp": { - "description": "The timestamp at which the total bond was registered.", - "allOf": [ - { - "$ref": "#/definitions/Timestamp" - } - ] - }, - "weight": { - "description": "The total weight of the bond at the given block height.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, "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" @@ -756,7 +736,7 @@ } } }, - "claimable_epochs": { + "claimable": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ClaimableEpochsResponse", "type": "object", @@ -765,6 +745,7 @@ ], "properties": { "epochs": { + "description": "The epochs that can be claimed by the address.", "type": "array", "items": { "$ref": "#/definitions/Epoch" @@ -897,9 +878,9 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", - "owner", "pool_manager_addr", "unbonding_period" ], @@ -915,6 +896,14 @@ "description": "Distribution denom for the rewards", "type": "string" }, + "epoch_manager_addr": { + "description": "Epoch Manager contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "grace_period": { "description": "The duration of the grace period in epochs, i.e. how many expired epochs can be claimed", "allOf": [ @@ -931,14 +920,6 @@ } ] }, - "owner": { - "description": "Owner of the contract.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, "pool_manager_addr": { "description": "Pool Manager contract address for swapping", "allOf": [ @@ -948,7 +929,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" @@ -1050,6 +1031,101 @@ } } }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, "total_bonded": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "BondedResponse", @@ -1057,21 +1133,34 @@ "type": "object", "required": [ "bonded_assets", - "first_bonded_epoch_id", "total_bonded" ], "properties": { "bonded_assets": { + "description": "The total amount of bonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] }, "total_bonded": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, @@ -1112,9 +1201,15 @@ ], "properties": { "total_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of unbonded tokens by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "unbonding_requests": { + "description": "The total amount of unbonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Bond" @@ -1205,19 +1300,40 @@ ], "properties": { "address": { + "description": "The weight of the address.", "type": "string" }, "global_weight": { - "$ref": "#/definitions/Uint128" + "description": "The global weight of the contract.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "share": { - "$ref": "#/definitions/Decimal" + "description": "The share the address has of the rewards at the particular timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, "timestamp": { - "$ref": "#/definitions/Timestamp" + "description": "The timestamp at which the weight was calculated.", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "weight": { - "$ref": "#/definitions/Uint128" + "description": "The weight of the address at the given timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, @@ -1254,7 +1370,12 @@ ], "properties": { "withdrawable_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of withdrawable assets by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json index bbc26076..9f165688 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/execute.json @@ -4,17 +4,10 @@ "oneOf": [ { "description": "Bonds the specified [Asset].", - "type": "object", - "required": [ + "type": "string", + "enum": [ "bond" - ], - "properties": { - "bond": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Unbonds the specified [Asset].", @@ -30,7 +23,12 @@ ], "properties": { "asset": { - "$ref": "#/definitions/Coin" + "description": "The asset to unbond.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] } }, "additionalProperties": false @@ -52,6 +50,7 @@ ], "properties": { "denom": { + "description": "The denom to withdraw.", "type": "string" } }, @@ -71,6 +70,7 @@ "type": "object", "properties": { "growth_rate": { + "description": "The new growth rate.", "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -80,19 +80,15 @@ } ] }, - "owner": { - "type": [ - "string", - "null" - ] - }, "pool_manager_addr": { + "description": "The new pool manager address.", "type": [ "string", "null" ] }, "unbonding_period": { + "description": "The unbonding period.", "anyOf": [ { "$ref": "#/definitions/Uint64" @@ -109,27 +105,21 @@ "additionalProperties": false }, { - "type": "object", - "required": [ + "description": "Claims the available rewards", + "type": "string", + "enum": [ "claim" - ], - "properties": { - "claim": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Fills the whale lair with new rewards.", + "description": "Fills the contract with new rewards.", "type": "string", "enum": [ "fill_rewards" ] }, { - "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", + "description": "Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment)", "type": "object", "required": [ "epoch_changed_hook" @@ -142,16 +132,85 @@ ], "properties": { "current_epoch": { - "$ref": "#/definitions/Epoch" + "description": "The current epoch, the one that was newly created.", + "allOf": [ + { + "$ref": "#/definitions/Epoch" + } + ] } }, "additionalProperties": false } }, "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false } ], "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, "Coin": { "type": "object", "required": [ @@ -189,6 +248,53 @@ }, "additionalProperties": false }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json index 3ffb9879..b0d74ffa 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/instantiate.json @@ -5,6 +5,7 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", "unbonding_period" @@ -21,6 +22,10 @@ "description": "Denom to be swapped to and rewarded", "type": "string" }, + "epoch_manager_addr": { + "description": "The epoch manager contract", + "type": "string" + }, "grace_period": { "description": "Grace period the maximum age of a bucket before fees are forwarded from it", "allOf": [ @@ -38,7 +43,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json index c016c928..1c84f130 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/query.json @@ -4,17 +4,10 @@ "oneOf": [ { "description": "Returns the [Config] of te contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Returns the amount of assets that have been bonded by the specified address.", @@ -25,12 +18,13 @@ "properties": { "bonded": { "type": "object", - "required": [ - "address" - ], "properties": { "address": { - "type": "string" + "description": "The address to check for bonded assets. If none is provided, all bonded assets in the contract are returned.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false @@ -53,12 +47,15 @@ ], "properties": { "address": { + "description": "The address to check for unbonding assets.", "type": "string" }, "denom": { + "description": "The denom to check for unbonding assets.", "type": "string" }, "limit": { + "description": "The maximum amount of unbonding assets to return.", "type": [ "integer", "null" @@ -67,6 +64,7 @@ "minimum": 0.0 }, "start_after": { + "description": "The amount of unbonding assets to skip. Allows pagination.", "type": [ "integer", "null" @@ -95,9 +93,11 @@ ], "properties": { "address": { + "description": "The address to check for withdrawable assets.", "type": "string" }, "denom": { + "description": "The denom to check for withdrawable assets.", "type": "string" } }, @@ -120,9 +120,11 @@ ], "properties": { "address": { + "description": "The address to check for weight.", "type": "string" }, "global_index": { + "description": "The global index to check for weight. If none is provided, the current global index is used.", "anyOf": [ { "$ref": "#/definitions/GlobalIndex" @@ -133,6 +135,7 @@ ] }, "timestamp": { + "description": "The timestamp to check for weight. If none is provided, the current block time is used.", "anyOf": [ { "$ref": "#/definitions/Timestamp" @@ -150,63 +153,50 @@ }, { "description": "Returns the total amount of assets that have been bonded to the contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "total_bonded" - ], - "properties": { - "total_bonded": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { "description": "Returns the global index of the contract.", - "type": "object", - "required": [ + "type": "string", + "enum": [ "global_index" - ], - "properties": { - "global_index": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + ] }, { - "description": "Returns the [Epoch]s that can be claimed.", + "description": "Returns the [Epoch]s that can be claimed by an address.", "type": "object", "required": [ - "claimable_epochs" + "claimable" ], "properties": { - "claimable_epochs": { + "claimable": { "type": "object", + "properties": { + "address": { + "description": "The address to check for claimable epochs. If none is provided, all possible epochs stored in the contract that can potentially be claimed are returned.", + "type": [ + "string", + "null" + ] + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Returns the [Epoch]s that can be claimed by an address.", + "description": "Query the contract's ownership information", "type": "object", "required": [ - "claimable" + "ownership" ], "properties": { - "claimable": { + "ownership": { "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "type": "string" - } - }, "additionalProperties": false } }, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json index 5b176215..d8a0ddbd 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_bonded.json @@ -5,21 +5,34 @@ "type": "object", "required": [ "bonded_assets", - "first_bonded_epoch_id", "total_bonded" ], "properties": { "bonded_assets": { + "description": "The total amount of bonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] }, "total_bonded": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json index 5084901f..6a728ff2 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_claimable.json @@ -7,6 +7,7 @@ ], "properties": { "epochs": { + "description": "The epochs that can be claimed by the address.", "type": "array", "items": { "$ref": "#/definitions/Epoch" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json index 2ef8b2bc..3d6850fc 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_config.json @@ -5,9 +5,9 @@ "required": [ "bonding_assets", "distribution_denom", + "epoch_manager_addr", "grace_period", "growth_rate", - "owner", "pool_manager_addr", "unbonding_period" ], @@ -23,6 +23,14 @@ "description": "Distribution denom for the rewards", "type": "string" }, + "epoch_manager_addr": { + "description": "Epoch Manager contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "grace_period": { "description": "The duration of the grace period in epochs, i.e. how many expired epochs can be claimed", "allOf": [ @@ -39,14 +47,6 @@ } ] }, - "owner": { - "description": "Owner of the contract.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, "pool_manager_addr": { "description": "Pool Manager contract address for swapping", "allOf": [ @@ -56,7 +56,7 @@ ] }, "unbonding_period": { - "description": "Unbonding period in nanoseconds.", + "description": "Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can be withdrawn", "allOf": [ { "$ref": "#/definitions/Uint64" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_ownership.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_ownership.json new file mode 100644 index 00000000..afe1713f --- /dev/null +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_ownership.json @@ -0,0 +1,95 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json index 5b176215..d8a0ddbd 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_total_bonded.json @@ -5,21 +5,34 @@ "type": "object", "required": [ "bonded_assets", - "first_bonded_epoch_id", "total_bonded" ], "properties": { "bonded_assets": { + "description": "The total amount of bonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Coin" } }, "first_bonded_epoch_id": { - "$ref": "#/definitions/Uint64" + "description": "If Some, the epoch id at which the user/address bonded first time. None is used when this Response is used to check the bonded assets in the contract.", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] }, "total_bonded": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of bonded tokens by the address. Bear in mind the bonded assets are considered to be equal for this purpose.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json index 29decae8..476b888b 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_unbonding.json @@ -9,9 +9,15 @@ ], "properties": { "total_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of unbonded tokens by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "unbonding_requests": { + "description": "The total amount of unbonded assets by the address.", "type": "array", "items": { "$ref": "#/definitions/Bond" diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json index 4355e1f9..0e017e02 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_weight.json @@ -12,19 +12,40 @@ ], "properties": { "address": { + "description": "The weight of the address.", "type": "string" }, "global_weight": { - "$ref": "#/definitions/Uint128" + "description": "The global weight of the contract.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "share": { - "$ref": "#/definitions/Decimal" + "description": "The share the address has of the rewards at the particular timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, "timestamp": { - "$ref": "#/definitions/Timestamp" + "description": "The timestamp at which the weight was calculated.", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "weight": { - "$ref": "#/definitions/Uint128" + "description": "The weight of the address at the given timestamp.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json index 79b3317c..42012032 100644 --- a/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json +++ b/contracts/liquidity_hub/bonding-manager/schema/raw/response_to_withdrawable.json @@ -8,7 +8,12 @@ ], "properties": { "withdrawable_amount": { - "$ref": "#/definitions/Uint128" + "description": "The total amount of withdrawable assets by the address.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] } }, "additionalProperties": false, diff --git a/contracts/liquidity_hub/bonding-manager/src/commands.rs b/contracts/liquidity_hub/bonding-manager/src/commands.rs index 25333f17..2f6457fa 100644 --- a/contracts/liquidity_hub/bonding-manager/src/commands.rs +++ b/contracts/liquidity_hub/bonding-manager/src/commands.rs @@ -3,11 +3,11 @@ use cosmwasm_std::{ StdError, StdResult, SubMsg, Timestamp, Uint128, Uint64, }; -use white_whale_std::bonding_manager::Bond; +use white_whale_std::bonding_manager::{Bond, Epoch, GlobalIndex}; use white_whale_std::pool_network::asset; use crate::helpers::validate_growth_rate; -use crate::queries::{get_current_epoch, query_claimable, query_weight, MAX_PAGE_LIMIT}; +use crate::queries::{get_expiring_epoch, query_claimable, query_weight, MAX_PAGE_LIMIT}; use crate::state::{ update_global_weight, update_local_weight, BOND, CONFIG, EPOCHS, GLOBAL, LAST_CLAIMED_EPOCH, UNBOND, @@ -22,8 +22,10 @@ pub(crate) fn bond( env: Env, asset: Coin, ) -> Result { + println!("bonding"); helpers::validate_claimed(&deps, &info)?; helpers::validate_bonding_for_current_epoch(&deps, &env)?; + println!("bonding 2"); let mut bond = BOND .key((&info.sender, &asset.denom)) .may_load(deps.storage)? @@ -37,15 +39,12 @@ pub(crate) fn bond( // update local values bond.asset.amount = bond.asset.amount.checked_add(asset.amount)?; - // let new_bond_weight = get_weight(timestamp, bond.weight, asset.amount, config.growth_rate, bond.timestamp)?; bond.weight = bond.weight.checked_add(asset.amount)?; bond = update_local_weight(&mut deps, info.sender.clone(), timestamp, bond)?; BOND.save(deps.storage, (&info.sender, &asset.denom), &bond)?; // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); - // global_index = update_global_weight(&mut deps, timestamp, global_index)?; - // move into one common func TODO: // include time term in the weight global_index.weight = global_index.weight.checked_add(asset.amount)?; global_index.bonded_amount = global_index.bonded_amount.checked_add(asset.amount)?; @@ -55,17 +54,6 @@ pub(crate) fn bond( GLOBAL.save(deps.storage, &global_index)?; - let epoch = get_current_epoch(deps.as_ref())?.epoch; - EPOCHS.update( - deps.storage, - &epoch.id.to_be_bytes(), - |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.global_index = global_index.clone(); - Ok(bucket) - }, - )?; - Ok(Response::default().add_attributes(vec![ ("action", "bond".to_string()), ("address", info.sender.to_string()), @@ -83,7 +71,7 @@ pub(crate) fn unbond( ) -> Result { ensure!( asset.amount > Uint128::zero(), - ContractError::InvalidUnbondingAmount {} + ContractError::InvalidUnbondingAmount ); helpers::validate_claimed(&deps, &info)?; @@ -95,14 +83,14 @@ pub(crate) fn unbond( // check if the address has enough bond ensure!( unbond.asset.amount >= asset.amount, - ContractError::InsufficientBond {} + ContractError::InsufficientBond ); // update local values, decrease the bond unbond = update_local_weight(&mut deps, info.sender.clone(), timestamp, unbond.clone())?; let weight_slash = unbond.weight * Decimal::from_ratio(asset.amount, unbond.asset.amount); - unbond.weight = unbond.weight.checked_sub(weight_slash)?; - unbond.asset.amount = unbond.asset.amount.checked_sub(asset.amount)?; + unbond.weight = unbond.weight.saturating_sub(weight_slash); + unbond.asset.amount = unbond.asset.amount.saturating_sub(asset.amount); if unbond.asset.amount.is_zero() { BOND.remove(deps.storage, (&info.sender, &asset.denom)); @@ -119,14 +107,13 @@ pub(crate) fn unbond( timestamp, }, )?; - // move this to a function to be reused // update global values let mut global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); global_index = update_global_weight(&mut deps, timestamp, global_index)?; - global_index.bonded_amount = global_index.bonded_amount.checked_sub(asset.amount)?; + global_index.bonded_amount = global_index.bonded_amount.saturating_sub(asset.amount); global_index.bonded_assets = white_whale_std::coin::deduct_coins(global_index.bonded_assets, vec![asset.clone()])?; - global_index.weight = global_index.weight.checked_sub(weight_slash)?; + global_index.weight = global_index.weight.saturating_sub(weight_slash); GLOBAL.save(deps.storage, &global_index)?; @@ -136,7 +123,7 @@ pub(crate) fn unbond( ("asset", asset.to_string()), ])) } else { - Err(ContractError::NothingToUnbond {}) + Err(ContractError::NothingToUnbond) } } @@ -157,7 +144,7 @@ pub(crate) fn withdraw( let mut refund_amount = Uint128::zero(); - ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw {}); + ensure!(!unbondings.is_empty(), ContractError::NothingToWithdraw); for unbonding in unbondings { let (ts, bond) = unbonding; @@ -192,25 +179,19 @@ pub(crate) fn withdraw( pub(crate) fn update_config( deps: DepsMut, info: MessageInfo, - owner: Option, pool_manager_addr: Option, unbonding_period: Option, growth_rate: Option, ) -> Result { // check the owner is the one who sent the message + cw_ownable::assert_owner(deps.storage, &info.sender)?; + let mut config = CONFIG.load(deps.storage)?; - if config.owner != info.sender { - return Err(ContractError::Unauthorized {}); - } if let Some(pool_manager_addr) = pool_manager_addr { config.pool_manager_addr = deps.api.addr_validate(&pool_manager_addr)?; } - if let Some(owner) = owner { - config.owner = deps.api.addr_validate(&owner)?; - } - if let Some(unbonding_period) = unbonding_period { config.unbonding_period = unbonding_period; } @@ -224,7 +205,6 @@ pub(crate) fn update_config( Ok(Response::default().add_attributes(vec![ ("action", "update_config".to_string()), - ("owner", config.owner.to_string()), ("pool_manager_addr", config.pool_manager_addr.to_string()), ("unbonding_period", config.unbonding_period.to_string()), ("growth_rate", config.growth_rate.to_string()), @@ -232,46 +212,64 @@ pub(crate) fn update_config( } /// Claims pending rewards for the sender. -pub fn claim(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { - let claimable_epochs = query_claimable(deps.as_ref(), &info.sender)?.epochs; +pub fn claim(deps: DepsMut, info: MessageInfo) -> Result { + let claimable_epochs_for_user = + query_claimable(deps.as_ref(), Some(info.sender.to_string()))?.epochs; ensure!( - !claimable_epochs.is_empty(), - ContractError::NothingToClaim {} + !claimable_epochs_for_user.is_empty(), + ContractError::NothingToClaim ); - let _global = GLOBAL.load(deps.storage)?; + let mut claimable_fees = vec![]; - for mut epoch in claimable_epochs.clone() { - let bonding_weight_response = query_weight( + let mut attributes = vec![]; + for mut epoch in claimable_epochs_for_user.clone() { + let bonding_weight_response_for_epoch = query_weight( deps.as_ref(), epoch.start_time, info.sender.to_string(), Some(epoch.global_index.clone()), )?; + // if the user has no share in the epoch, skip it + if bonding_weight_response_for_epoch.share.is_zero() { + continue; + }; + + // sanity check + ensure!( + bonding_weight_response_for_epoch.share <= Decimal::percent(100u64), + ContractError::InvalidShare + ); + for fee in epoch.total.iter() { - let reward = fee.amount * bonding_weight_response.share; + let reward = fee.amount * bonding_weight_response_for_epoch.share; - if reward.is_zero() { - // nothing to claim - continue; - } // make sure the reward is sound - let _ = epoch + let reward_validation: Result<(), StdError> = epoch .available .iter() .find(|available_fee| available_fee.denom == fee.denom) .map(|available_fee| { if reward > available_fee.amount { - //todo maybe we can just skip this epoch and log something on the attributes instead - // of returning an error and blocking the whole operation - // this would "solve" the case when users unbond and then those who have not claimed - // past epochs won't be able to do it as their rewards exceed the available claimable fees - // cuz their weight increased in relation to the global weight - return Err(ContractError::InvalidReward {}); + attributes.push(( + "error", + ContractError::InvalidReward { + reward, + available: available_fee.amount, + } + .to_string(), + )); } Ok(()) }) - .ok_or_else(|| StdError::generic_err("Invalid fee"))?; + .ok_or(StdError::generic_err("Invalid fee"))?; + + // if the reward is invalid, skip the epoch + match reward_validation { + Ok(_) => {} + Err(_) => continue, + } + let denom = &fee.denom; // add the reward to the claimable fees claimable_fees = asset::aggregate_coins( @@ -285,7 +283,7 @@ pub fn claim(deps: DepsMut, _env: Env, info: MessageInfo) -> Result Result Result Result { + println!( + "EPOCHS: {:?}", + EPOCHS + .keys(deps.storage, None, None, Order::Descending) + .collect::>() + ); + // Finding the most recent EpochID - let most_recent_epoch_id = match EPOCHS + let upcoming_epoch_id = match EPOCHS .keys(deps.storage, None, None, Order::Descending) .next() { Some(epoch_id) => epoch_id?, - None => return Err(ContractError::Unauthorized {}), + None => return Err(ContractError::Unauthorized), }; let config = CONFIG.load(deps.storage)?; @@ -354,7 +365,8 @@ pub(crate) fn fill_rewards( }) .to_owned(); - // coins that are laying in the contract and have not been swapped before for lack of swap routes + // coins (not the distribution_denom) that are laying in the contract and have not been swapped before for lack + // of swap routes let remanent_coins = deps .querier .query_all_balances(env.contract.address)? @@ -365,7 +377,7 @@ pub(crate) fn fill_rewards( println!("remanent_coins: {:?}", remanent_coins); // Each of these helpers will add messages to the messages vector // and may increment the whale Coin above with the result of the swaps - helpers::handle_lp_tokens(&info, &config, &mut submessages)?; + helpers::handle_lp_tokens(&remanent_coins, &config, &mut submessages)?; helpers::swap_coins_to_main_token( remanent_coins, &deps, @@ -374,22 +386,132 @@ pub(crate) fn fill_rewards( &distribution_denom, &mut messages, )?; + + println!("here"); // Add the whale to the funds, the whale figure now should be the result // of all the LP token withdrawals and swaps // Because we are using minimum receive, it is possible the contract can accumulate micro amounts of whale if we get more than what the swap query returned // If this became an issue would could look at replys instead of the query - EPOCHS.update( - deps.storage, - &most_recent_epoch_id, - |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; - bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; - Ok(bucket) - }, - )?; + EPOCHS.update(deps.storage, &upcoming_epoch_id, |bucket| -> StdResult<_> { + let mut bucket = bucket.unwrap_or_default(); + bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; + bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; + Ok(bucket) + })?; Ok(Response::default() .add_messages(messages) .add_submessages(submessages) .add_attributes(vec![("action", "fill_rewards".to_string())])) } + +pub(crate) fn on_epoch_created( + deps: DepsMut, + env: Env, + info: MessageInfo, + current_epoch: white_whale_std::epoch_manager::epoch_manager::Epoch, +) -> Result { + cw_utils::nonpayable(&info)?; + + println!("EpochChangedHook: {:?}", current_epoch); + // A new epoch has been created, update rewards bucket and forward the expiring epoch + // Store epoch and verify the sender is the epoch manager + let config = CONFIG.load(deps.storage)?; + ensure!( + info.sender == config.epoch_manager_addr, + ContractError::Unauthorized + ); + + let global = GLOBAL.may_load(deps.storage)?; + // This happens only on the very first epoch where Global has not been initialised yet + if global.is_none() { + let initial_global_index = GlobalIndex { + timestamp: env.block.time, + ..Default::default() + }; + GLOBAL.save(deps.storage, &initial_global_index)?; + EPOCHS.save( + deps.storage, + ¤t_epoch.id.to_be_bytes(), + &Epoch { + id: current_epoch.id.into(), + start_time: current_epoch.start_time, + global_index: initial_global_index, + ..Epoch::default() + }, + )?; + } + + let global = GLOBAL.load(deps.storage)?; + + // update the global index for the current epoch, take the current snapshot of the global index + EPOCHS.update( + deps.storage, + ¤t_epoch.id.to_be_bytes(), + |epoch| -> StdResult<_> { + let mut epoch = epoch.unwrap_or_default(); + epoch.global_index = global; + Ok(epoch) + }, + )?; + + // todo to delete once the testing is done + let all_epochs: Vec = EPOCHS + .range(deps.storage, None, None, Order::Descending) + .map(|item| { + let (_, epoch) = item?; + Ok(epoch) + }) + .collect::>>()?; + + println!("EPOCHS: {:?}", all_epochs); + + // forward fees from the expiring epoch to the new one. + let mut expiring_epoch = get_expiring_epoch(deps.as_ref())?; + if let Some(expiring_epoch) = expiring_epoch.as_mut() { + // Load all the available assets from the expiring epoch + let amount_to_be_forwarded = EPOCHS + .load(deps.storage, &expiring_epoch.id.to_be_bytes())? + .available; + EPOCHS.update( + deps.storage, + ¤t_epoch.id.to_be_bytes(), + |epoch| -> StdResult<_> { + let mut epoch = epoch.unwrap_or_default(); + epoch.available = + asset::aggregate_coins(epoch.available, amount_to_be_forwarded.clone())?; + epoch.total = asset::aggregate_coins(epoch.total, amount_to_be_forwarded)?; + + Ok(epoch) + }, + )?; + // Set the available assets for the expiring epoch to an empty vec now that they have been + // forwarded + EPOCHS.update( + deps.storage, + &expiring_epoch.id.to_be_bytes(), + |epoch| -> StdResult<_> { + let mut epoch = epoch.unwrap_or_default(); + epoch.available = vec![]; + Ok(epoch) + }, + )?; + } + + // Create a new bucket for the rewards flowing from this time on, i.e. to be distributed in + // the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) + let next_epoch_id = Uint64::new(current_epoch.id).checked_add(Uint64::one())?; + EPOCHS.save( + deps.storage, + &next_epoch_id.u64().to_be_bytes(), + &Epoch { + id: next_epoch_id, + start_time: current_epoch.start_time.plus_days(1), + // this global index is to be updated the next time this hook is called, as this future epoch + // will become the current one + global_index: Default::default(), + ..Epoch::default() + }, + )?; + + Ok(Response::default().add_attributes(vec![("action", "epoch_changed_hook".to_string())])) +} diff --git a/contracts/liquidity_hub/bonding-manager/src/contract.rs b/contracts/liquidity_hub/bonding-manager/src/contract.rs index bc30b780..f69f1e05 100644 --- a/contracts/liquidity_hub/bonding-manager/src/contract.rs +++ b/contracts/liquidity_hub/bonding-manager/src/contract.rs @@ -1,17 +1,14 @@ -use cosmwasm_std::{entry_point, from_json, Addr, Coin, Order, Reply, Uint128}; +use cosmwasm_std::{ensure, entry_point, from_json, Addr, Coin, Order, Reply, Uint128}; use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::{get_contract_version, set_contract_version}; use cw_utils::parse_reply_execute_data; use white_whale_std::pool_network::asset; -use white_whale_std::bonding_manager::{ - Config, Epoch, ExecuteMsg, GlobalIndex, InstantiateMsg, MigrateMsg, QueryMsg, -}; +use white_whale_std::bonding_manager::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::error::ContractError; use crate::helpers::{self, validate_growth_rate}; -use crate::queries::get_expiring_epoch; -use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, EPOCHS, GLOBAL}; +use crate::state::{BONDING_ASSETS_LIMIT, CONFIG, EPOCHS}; use crate::{commands, queries}; // version info for migration info @@ -27,20 +24,17 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - if msg.bonding_assets.len() > BONDING_ASSETS_LIMIT { - return Err(ContractError::InvalidBondingAssetsLimit( - BONDING_ASSETS_LIMIT, - msg.bonding_assets.len(), - )); - } + ensure!( + msg.bonding_assets.len() <= BONDING_ASSETS_LIMIT, + ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, msg.bonding_assets.len(),) + ); validate_growth_rate(msg.growth_rate)?; - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let config = Config { - owner: deps.api.addr_validate(info.sender.as_str())?, pool_manager_addr: Addr::unchecked(""), + epoch_manager_addr: Addr::unchecked(""), distribution_denom: msg.distribution_denom, unbonding_period: msg.unbonding_period, growth_rate: msg.growth_rate, @@ -49,21 +43,11 @@ pub fn instantiate( }; CONFIG.save(deps.storage, &config)?; - // Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) - // Add a new rewards bucket for the new epoch - // EPOCHS.save( - // deps.storage, - // &0u64.to_be_bytes(), - // &Epoch { - // id: 0u64.into(), - // start_time: env.block.time, - // ..Epoch::default() - // }, - // )?; - // GLOBAL.save(deps.storage, &GlobalIndex{ bonded_amount: Uint128::zero(), bonded_assets: vec![], timestamp: env.block.time, weight: Uint128::zero() })?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; + Ok(Response::default().add_attributes(vec![ ("action", "instantiate".to_string()), - ("owner", config.owner.to_string()), + ("owner", info.sender.to_string()), ("unbonding_period", config.unbonding_period.to_string()), ("growth_rate", config.growth_rate.to_string()), ("bonding_assets", msg.bonding_assets.join(", ")), @@ -79,7 +63,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Bond {} => { + ExecuteMsg::Bond => { let asset_to_bond = helpers::validate_funds(&deps, &info)?; commands::bond(deps, env.block.time, info, env, asset_to_bond) } @@ -92,154 +76,44 @@ pub fn execute( commands::withdraw(deps, env.block.time, info.sender, denom) } ExecuteMsg::UpdateConfig { - owner, pool_manager_addr, unbonding_period, growth_rate, - } => commands::update_config( - deps, - info, - owner, - pool_manager_addr, - unbonding_period, - growth_rate, - ), + } => { + cw_utils::nonpayable(&info)?; + commands::update_config(deps, info, pool_manager_addr, unbonding_period, growth_rate) + } ExecuteMsg::FillRewards => commands::fill_rewards(deps, env, info), - ExecuteMsg::Claim { .. } => commands::claim(deps, env, info), + ExecuteMsg::Claim => commands::claim(deps, info), ExecuteMsg::EpochChangedHook { current_epoch } => { - println!("EpochChangedHook: {:?}", current_epoch); - // Epoch has been updated, update rewards bucket - // and forward the expiring epoch - // Store epoch manager and verify the sender is him - let global = GLOBAL.may_load(deps.storage)?; - // This happens only on the first epoch where Global has not been initialised yet - if global.is_none() { - let default_global = GlobalIndex { - timestamp: env.block.time, - ..Default::default() - }; - GLOBAL.save(deps.storage, &default_global)?; - EPOCHS.save( - deps.storage, - ¤t_epoch.id.to_be_bytes(), - &Epoch { - id: current_epoch.id.into(), - start_time: current_epoch.start_time, - global_index: default_global, - ..Epoch::default() - }, - )?; - } - let global = GLOBAL.load(deps.storage)?; - - // Review, what if current_epoch form the hook is actually next_epoch_id and then epoch - 1 would be previous one - let new_epoch_id = current_epoch.id; - let next_epoch_id = match new_epoch_id.checked_add(1u64) { - Some(next_epoch_id) => next_epoch_id, - None => return Err(ContractError::Unauthorized {}), - }; - // Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) - // Add a new rewards bucket for the new epoch - EPOCHS.save( - deps.storage, - &next_epoch_id.to_be_bytes(), - &Epoch { - id: next_epoch_id.into(), - start_time: current_epoch.start_time.plus_days(1), - global_index: global, - ..Epoch::default() - }, - )?; - - let all_epochs: Vec = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .map(|item| { - let (_, epoch) = item?; - Ok(epoch) - }) - .collect::>>()?; - - println!("EPOCHS: {:?}", all_epochs); - - // // Return early if the epoch is the first one - // if new_epoch_id == 1 { - // // Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) - // // Add a new rewards bucket for the new epoch - // EPOCHS.save( - // deps.storage, - // &new_epoch_id.to_be_bytes(), - // &Epoch { - // id: next_epoch_id.into(), - // start_time: current_epoch.start_time, - // global_index: global.clone(), - // ..Epoch::default() - // }, - // )?; - // return Ok(Response::default() - // .add_attributes(vec![("action", "epoch_changed_hook".to_string())])); - // } - - // forward fees from the expiring epoch to the new one. - let mut expiring_epoch = get_expiring_epoch(deps.as_ref())?; - if let Some(expiring_epoch) = expiring_epoch.as_mut() { - // Load all the available assets from the expiring epoch - let amount_to_be_forwarded = EPOCHS - .load(deps.storage, &expiring_epoch.id.to_be_bytes())? - .available; - EPOCHS.update( - deps.storage, - &new_epoch_id.to_be_bytes(), - |epoch| -> StdResult<_> { - let mut epoch = epoch.unwrap_or_default(); - epoch.available = asset::aggregate_coins( - epoch.available, - amount_to_be_forwarded.clone(), - )?; - epoch.total = asset::aggregate_coins(epoch.total, amount_to_be_forwarded)?; - - Ok(epoch) - }, - )?; - // Set the available assets for the expiring epoch to an empty vec now that they have been forwarded - EPOCHS.update( - deps.storage, - &expiring_epoch.id.to_be_bytes(), - |epoch| -> StdResult<_> { - let mut epoch = epoch.unwrap_or_default(); - epoch.available = vec![]; - Ok(epoch) - }, - )?; - } - - Ok(Response::default() - .add_attributes(vec![("action", "epoch_changed_hook".to_string())])) + commands::on_epoch_created(deps, env, info, current_epoch) + } + ExecuteMsg::UpdateOwnership(action) => { + cw_utils::nonpayable(&info)?; + white_whale_std::common::update_ownership(deps, env, info, action).map_err(Into::into) } } } #[entry_point] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::Config {} => to_json_binary(&queries::query_config(deps)?), - QueryMsg::Bonded { address } => to_json_binary(&queries::query_bonded(deps, address)?), + QueryMsg::Config => Ok(to_json_binary(&queries::query_config(deps)?)?), + QueryMsg::Bonded { address } => Ok(to_json_binary(&queries::query_bonded(deps, address)?)?), QueryMsg::Unbonding { address, denom, start_after, limit, - } => to_json_binary(&queries::query_unbonding( + } => Ok(to_json_binary(&queries::query_unbonding( deps, address, denom, start_after, limit, - )?), - QueryMsg::Withdrawable { address, denom } => to_json_binary(&queries::query_withdrawable( - deps, - env.block.time, - address, - denom, + )?)?), + QueryMsg::Withdrawable { address, denom } => Ok(to_json_binary( + &queries::query_withdrawable(deps, env.block.time, address, denom)?, )?), QueryMsg::Weight { address, @@ -250,20 +124,19 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { let timestamp = timestamp.unwrap_or(env.block.time); // TODO: Make better timestamp handling - to_json_binary(&queries::query_weight( + Ok(to_json_binary(&queries::query_weight( deps, timestamp, address, global_index, - )?) + )?)?) } - QueryMsg::TotalBonded {} => to_json_binary(&queries::query_total_bonded(deps)?), - QueryMsg::GlobalIndex {} => to_json_binary(&queries::query_global_index(deps)?), - QueryMsg::Claimable { addr } => to_json_binary(&queries::query_claimable( - deps, - &deps.api.addr_validate(&addr)?, - )?), - QueryMsg::ClaimableEpochs {} => to_json_binary(&queries::get_claimable_epochs(deps)?), + QueryMsg::TotalBonded => Ok(to_json_binary(&queries::query_total_bonded(deps)?)?), + QueryMsg::GlobalIndex => Ok(to_json_binary(&queries::query_global_index(deps)?)?), + QueryMsg::Claimable { address } => { + Ok(to_json_binary(&queries::query_claimable(deps, address)?)?) + } + QueryMsg::Ownership {} => Ok(to_json_binary(&cw_ownable::get_ownership(deps.storage)?)?), } } @@ -276,35 +149,16 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result = from_json(data.as_slice())?; let config = CONFIG.load(deps.storage)?; let distribution_denom = config.distribution_denom.clone(); let mut messages = vec![]; - // // Loop msg events to find the transfer event and the assets received - // for event in msg.result.unwrap().events { - // if event.ty == "transfer" { - // let attributes = event.attributes; - // for attr in attributes { - // if attr.key == "amount" { - // let amount_str = attr.value; - // let amounts: Vec<&str> = amount_str.split(',').collect(); - // println!("Amounts: {:?}", amounts); - // for amount in amounts { - // // XXXXucoin is the format at this point, pass it to from_str to get the Coin struct - // coins.push(Coin::from_str(amount).unwrap()); - // } - // } - // } - // } - // } - - // Instead of going over events - // - // Search received coins funds for the distribution denom - let mut whale = coins + // Search received coins funds for the coin that is not the distribution denom + // This will be swapped for + let mut to_be_distribution_asset = coins .iter() .find(|coin| coin.denom.ne(distribution_denom.as_str())) .unwrap_or(&Coin { @@ -312,35 +166,48 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result epoch_id?, - None => return Err(ContractError::Unauthorized {}), - }; - EPOCHS.update(deps.storage, &next_epoch_id, |bucket| -> StdResult<_> { - let mut bucket = bucket.unwrap_or_default(); - bucket.available = asset::aggregate_coins(bucket.available, vec![whale.clone()])?; - bucket.total = asset::aggregate_coins(bucket.total, vec![whale.clone()])?; - Ok(bucket) - })?; + + // if the swap was successful and the to_be_distribution_asset.denom is the + // distribution_denom, update the upcoming epoch with the new funds + if to_be_distribution_asset.denom == distribution_denom { + // Finding the upcoming EpochID + let upcoming_epoch_id = match EPOCHS + .keys(deps.storage, None, None, Order::Descending) + .next() + { + Some(epoch_id) => epoch_id?, + None => return Err(ContractError::Unauthorized), + }; + + EPOCHS.update(deps.storage, &upcoming_epoch_id, |epoch| -> StdResult<_> { + let mut upcoming_epoch = epoch.unwrap_or_default(); + upcoming_epoch.available = asset::aggregate_coins( + upcoming_epoch.available, + vec![to_be_distribution_asset.clone()], + )?; + upcoming_epoch.total = asset::aggregate_coins( + upcoming_epoch.total, + vec![to_be_distribution_asset.clone()], + )?; + Ok(upcoming_epoch) + })?; + } Ok(Response::new() .add_messages(messages) .add_attribute("total_withdrawn", msg.id.to_string())) } - _ => Err(ContractError::Unauthorized {}), + _ => Err(ContractError::Unauthorized), } } diff --git a/contracts/liquidity_hub/bonding-manager/src/error.rs b/contracts/liquidity_hub/bonding-manager/src/error.rs index e8685266..9a44ea25 100644 --- a/contracts/liquidity_hub/bonding-manager/src/error.rs +++ b/contracts/liquidity_hub/bonding-manager/src/error.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{DivideByZeroError, OverflowError, StdError}; +use cosmwasm_std::{DivideByZeroError, OverflowError, StdError, Uint128}; +use cw_ownable::OwnershipError; use cw_utils::PaymentError; use semver::Version; use thiserror::Error; @@ -9,7 +10,10 @@ pub enum ContractError { Std(#[from] StdError), #[error("Unauthorized")] - Unauthorized {}, + Unauthorized, + + #[error("{0}")] + OwnershipError(#[from] OwnershipError), #[error("{0}")] PaymentError(#[from] PaymentError), @@ -18,13 +22,13 @@ pub enum ContractError { SemVer(String), #[error("The asset sent doesn't match the asset expected. Please check the denom and amount.")] - AssetMismatch {}, + AssetMismatch, #[error("The amount of tokens to unbond is greater than the amount of tokens bonded.")] - InsufficientBond {}, + InsufficientBond, #[error("The amount of tokens to unbond must be greater than zero.")] - InvalidUnbondingAmount {}, + InvalidUnbondingAmount, #[error("{0}")] DivideByZeroError(#[from] DivideByZeroError), @@ -33,21 +37,18 @@ pub enum ContractError { OverflowError(#[from] OverflowError), #[error("The growth rate must be between 0 and 1. i.e. 0.5 for 50%")] - InvalidGrowthRate {}, + InvalidGrowthRate, #[error( "The amount of bonding assets is greater than the limit allowed. Limit is {0}, sent {1}." )] InvalidBondingAssetsLimit(usize, usize), - #[error("Can only bond native assets.")] - InvalidBondingAsset {}, - - #[error("Nothing to unbond.")] - NothingToUnbond {}, + #[error("Nothing to unbond")] + NothingToUnbond, - #[error("Nothing to withdraw.")] - NothingToWithdraw {}, + #[error("Nothing to withdraw")] + NothingToWithdraw, #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] MigrateInvalidVersion { @@ -56,19 +57,21 @@ pub enum ContractError { }, #[error("There are unclaimed rewards available. Claim them before attempting to bond/unbond")] - UnclaimedRewards {}, + UnclaimedRewards, #[error("Trying to bond/unbond at a late time before the new/latest epoch has been created")] - NewEpochNotCreatedYet {}, + NewEpochNotCreatedYet, #[error("Nothing to claim")] - NothingToClaim {}, + NothingToClaim, - #[error("Nothing to claim")] - InvalidReward {}, + #[error("Something is off with the reward calculation, user share is above 1. Can't claim.")] + InvalidShare, - #[error("No Swap Route found for assets {asset1} and {asset2}")] - NoSwapRoute { asset1: String, asset2: String }, + #[error( + "Invalid reward amount. Reward: {reward}, but only {available} available in the epoch." + )] + InvalidReward { reward: Uint128, available: Uint128 }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/bonding-manager/src/helpers.rs b/contracts/liquidity_hub/bonding-manager/src/helpers.rs index 390016e5..9043699a 100644 --- a/contracts/liquidity_hub/bonding-manager/src/helpers.rs +++ b/contracts/liquidity_hub/bonding-manager/src/helpers.rs @@ -3,23 +3,24 @@ use cosmwasm_std::{ StdResult, SubMsg, Timestamp, Uint64, WasmMsg, }; use cw_utils::PaymentError; -use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Config, EpochResponse}; -use white_whale_std::constants::LP_SYMBOL; -use white_whale_std::epoch_manager::epoch_manager::EpochConfig; +use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Config}; +use white_whale_std::constants::{DAY_IN_SECONDS, LP_SYMBOL}; +use white_whale_std::epoch_manager::epoch_manager::{EpochConfig, EpochResponse}; use white_whale_std::pool_manager::{ PoolInfoResponse, SimulateSwapOperationsResponse, SwapRouteResponse, }; use crate::contract::LP_WITHDRAWAL_REPLY_ID; use crate::error::ContractError; -use crate::queries::{get_claimable_epochs, get_current_epoch}; +use crate::queries::query_claimable; use crate::state::CONFIG; /// Validates that the growth rate is between 0 and 1. pub fn validate_growth_rate(growth_rate: Decimal) -> Result<(), ContractError> { - if growth_rate > Decimal::percent(100) { - return Err(ContractError::InvalidGrowthRate {}); - } + ensure!( + growth_rate <= Decimal::percent(100), + ContractError::InvalidGrowthRate + ); Ok(()) } @@ -52,41 +53,46 @@ pub fn validate_funds(deps: &DepsMut, info: &MessageInfo) -> Result Result<(), ContractError> { +pub fn validate_claimed(deps: &DepsMut, info: &MessageInfo) -> Result<(), ContractError> { // Do a smart query for Claimable - let claimable_rewards: ClaimableEpochsResponse = get_claimable_epochs(deps.as_ref()).unwrap(); - // If epochs is greater than none - if !claimable_rewards.epochs.is_empty() { - return Err(ContractError::UnclaimedRewards {}); - } + let claimable_rewards: ClaimableEpochsResponse = + query_claimable(deps.as_ref(), Some(info.sender.to_string())).unwrap(); + // ensure the user has nothing to claim + ensure!( + claimable_rewards.epochs.is_empty(), + ContractError::UnclaimedRewards + ); Ok(()) } /// Validates that the current time is not more than a day after the epoch start time. Helps preventing /// global_index timestamp issues when querying the weight. -/// global_index timestamp issues when querying the weight. pub fn validate_bonding_for_current_epoch(deps: &DepsMut, env: &Env) -> Result<(), ContractError> { - let epoch_response: EpochResponse = get_current_epoch(deps.as_ref()).unwrap(); + let config = CONFIG.load(deps.storage)?; + let epoch_response: EpochResponse = deps.querier.query_wasm_smart( + config.epoch_manager_addr.to_string(), + &white_whale_std::epoch_manager::epoch_manager::QueryMsg::CurrentEpoch {}, + )?; let current_epoch = epoch_response.epoch; - let current_time = env.block.time.seconds(); - const DAY_IN_SECONDS: u64 = 86_400u64; - // Check if the current time is more than a day after the epoch start time - // to avoid potential overflow - if current_epoch.id != Uint64::zero() { + if current_epoch.id != 0u64 { + let current_time = env.block.time.seconds(); + let start_time_seconds = current_epoch .start_time .seconds() .checked_add(DAY_IN_SECONDS); + match start_time_seconds { Some(start_time_plus_day) => { - if current_time > start_time_plus_day { - return Err(ContractError::NewEpochNotCreatedYet {}); - } + ensure!( + current_time <= start_time_plus_day, + ContractError::NewEpochNotCreatedYet + ); } - None => return Err(ContractError::Unauthorized {}), + None => return Err(ContractError::Unauthorized), } } @@ -107,6 +113,7 @@ pub fn calculate_epoch( let elapsed_time = Uint64::new(timestamp.nanos()).checked_sub(genesis_epoch_config.genesis_epoch)?; + let epoch = elapsed_time .checked_div(epoch_duration)? .checked_add(Uint64::one())?; @@ -117,20 +124,23 @@ pub fn calculate_epoch( // Used in FillRewards to search the funds for LP tokens and withdraw them // If we do get some LP tokens to withdraw they could be swapped to whale in the reply pub fn handle_lp_tokens( - info: &MessageInfo, - config: &white_whale_std::bonding_manager::Config, + funds: &Vec, + config: &Config, submessages: &mut Vec, ) -> Result<(), ContractError> { - let lp_tokens: Vec<&Coin> = info - .funds + println!("funds: {:?}", funds); + let lp_tokens: Vec<&Coin> = funds .iter() .filter(|coin| coin.denom.contains(".pool.") | coin.denom.contains(LP_SYMBOL)) .collect(); + + println!("lp_tokens: {:?}", lp_tokens); + for lp_token in lp_tokens { - // LP tokens have the format "{pair_label}.pool.{identifier}.{LP_SYMBOL}", get the identifier and not the LP SYMBOL - let pool_identifier = lp_token.denom.split(".pool.").collect::>()[1] - .split('.') - .collect::>()[0]; + let pool_identifier = + extract_pool_identifier(&lp_token.denom).ok_or(ContractError::AssetMismatch)?; + + println!("pool_identifier: {:?}", pool_identifier); // if LP Tokens ,verify and withdraw then swap to whale let lp_withdrawal_msg = white_whale_std::pool_manager::ExecuteMsg::WithdrawLiquidity { @@ -153,12 +163,31 @@ pub fn handle_lp_tokens( Ok(()) } -// Used in FillRewards to search the funds for coins that are neither LP tokens nor whale and swap them to whale +/// Extracts the pool identifier from an LP token denom. +/// LP tokens have the format "{pair_label}.pool.{identifier}.{LP_SYMBOL}", get the +/// identifier and not the LP SYMBOL. The identifier can contain dots, slashes, etc. +fn extract_pool_identifier(lp_token_denom: &str) -> Option<&str> { + // Split the string at ".pool." to isolate the part after ".pool." + let parts: Vec<&str> = lp_token_denom.splitn(2, ".pool.").collect(); + if parts.len() < 2 { + return None; + } + + // Split by the last dot to isolate the identifier from "{LP_SYMBOL}" + let after_pool = parts[1]; + let last_dot_pos = after_pool.rfind('.').unwrap_or(after_pool.len()); + + // Take everything before the last dot to get the identifier + Some(&after_pool[..last_dot_pos]) +} + +// Used in FillRewards to search the funds for coins that are neither LP tokens nor the distribution_denom +// and swap them to distribution_denom pub fn swap_coins_to_main_token( coins: Vec, deps: &DepsMut, config: Config, - whale: &mut Coin, + to_be_distribution_asset: &mut Coin, distribution_denom: &String, messages: &mut Vec, ) -> Result<(), ContractError> { @@ -198,7 +227,8 @@ pub fn swap_coins_to_main_token( } // check if the pool has any assets, if not skip the swap - // Note we are only checking the first operation here. Might be better to another loop to check all operations + // Note we are only checking the first operation here. + // Might be better to another loop to check all operations let pool_query = white_whale_std::pool_manager::QueryMsg::Pool { pool_identifier: swap_routes .swap_route @@ -219,32 +249,36 @@ pub fn swap_coins_to_main_token( } }); - let simulate: SimulateSwapOperationsResponse = deps.querier.query_wasm_smart( - config.pool_manager_addr.to_string(), - &white_whale_std::pool_manager::QueryMsg::SimulateSwapOperations { - offer_amount: coin.amount, - operations: swap_routes.swap_route.swap_operations.clone(), - }, - )?; - // Add the simulate amount received to the whale amount, if the swap fails this should also be rolled back - whale.amount = whale.amount.checked_add(simulate.amount)?; + let simulate_swap_operations_response: SimulateSwapOperationsResponse = + deps.querier.query_wasm_smart( + config.pool_manager_addr.to_string(), + &white_whale_std::pool_manager::QueryMsg::SimulateSwapOperations { + offer_amount: coin.amount, + operations: swap_routes.swap_route.swap_operations.clone(), + }, + )?; + // Add the simulate amount received to the distribution_denom amount, if the swap fails this should + // also be rolled back + to_be_distribution_asset.amount = to_be_distribution_asset + .amount + .checked_add(simulate_swap_operations_response.amount)?; if !skip_swap { // Prepare a swap message, use the simulate amount as the minimum receive // and 1% slippage to ensure we get at least what was simulated to be received - let msg = white_whale_std::pool_manager::ExecuteMsg::ExecuteSwapOperations { - operations: swap_routes.swap_route.swap_operations.clone(), - minimum_receive: Some(simulate.amount), - receiver: None, - max_spread: Some(Decimal::percent(5)), - }; - let binary_msg = to_json_binary(&msg)?; - let wrapped_msg = WasmMsg::Execute { + let swap_msg = WasmMsg::Execute { contract_addr: config.pool_manager_addr.to_string(), - msg: binary_msg, + msg: to_json_binary( + &white_whale_std::pool_manager::ExecuteMsg::ExecuteSwapOperations { + operations: swap_routes.swap_route.swap_operations.clone(), + minimum_receive: Some(simulate_swap_operations_response.amount), + receiver: None, + max_spread: Some(Decimal::percent(5)), + }, + )?, funds: vec![coin.clone()], }; - messages.push(wrapped_msg.into()); + messages.push(swap_msg.into()); } } Ok(()) diff --git a/contracts/liquidity_hub/bonding-manager/src/queries.rs b/contracts/liquidity_hub/bonding-manager/src/queries.rs index 82cd8726..c5bb9d31 100644 --- a/contracts/liquidity_hub/bonding-manager/src/queries.rs +++ b/contracts/liquidity_hub/bonding-manager/src/queries.rs @@ -2,8 +2,8 @@ use std::collections::{HashSet, VecDeque}; use white_whale_std::epoch_manager::epoch_manager::ConfigResponse; use cosmwasm_std::{ - to_json_binary, Addr, Decimal, Deps, Order, QueryRequest, StdError, StdResult, Timestamp, - Uint128, Uint64, WasmQuery, + to_json_binary, Decimal, Deps, Order, QueryRequest, StdError, StdResult, Timestamp, Uint128, + Uint64, WasmQuery, }; use cw_storage_plus::Bound; @@ -11,7 +11,7 @@ use white_whale_std::bonding_manager::{ Bond, BondedResponse, BondingWeightResponse, Config, GlobalIndex, UnbondingResponse, WithdrawableResponse, }; -use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch, EpochResponse}; +use white_whale_std::bonding_manager::{ClaimableEpochsResponse, Epoch}; use white_whale_std::epoch_manager::epoch_manager::QueryMsg; use crate::helpers; @@ -25,52 +25,60 @@ pub(crate) fn query_config(deps: Deps) -> StdResult { } /// Queries the current bonded amount of the given address. -pub(crate) fn query_bonded(deps: Deps, address: String) -> StdResult { - let address = deps.api.addr_validate(&address)?; - - let bonds: Vec = BOND - .prefix(&address) - .range(deps.storage, None, None, Order::Ascending) - .take(BONDING_ASSETS_LIMIT) - .map(|item| { - let (_, bond) = item?; - Ok(bond) - }) - .collect::>>()?; +pub(crate) fn query_bonded(deps: Deps, address: Option) -> StdResult { + let (total_bonded, bonded_assets, first_bonded_epoch_id) = if let Some(address) = address { + let address = deps.api.addr_validate(&address)?; + + let bonds: Vec = BOND + .prefix(&address) + .range(deps.storage, None, None, Order::Ascending) + .take(BONDING_ASSETS_LIMIT) + .map(|item| { + let (_, bond) = item?; + Ok(bond) + }) + .collect::>>()?; + + // if it doesn't have bonded, return empty response + if bonds.is_empty() { + return Ok(BondedResponse { + total_bonded: Uint128::zero(), + bonded_assets: vec![], + first_bonded_epoch_id: Some(Uint64::zero()), + }); + } - // if it doesn't have bonded, return empty response - if bonds.is_empty() { - return Ok(BondedResponse { - total_bonded: Uint128::zero(), - bonded_assets: vec![], - first_bonded_epoch_id: Uint64::zero(), - }); - } + let mut total_bonded = Uint128::zero(); + let mut bonded_assets = vec![]; - let mut total_bonded = Uint128::zero(); - let mut bonded_assets = vec![]; + // 1 January 2500 + let mut first_bond_timestamp = Timestamp::from_seconds(16725229261u64); - let mut first_bond_timestamp = Timestamp::from_seconds(16725229261u64); + for bond in bonds { + if bond.timestamp.seconds() < first_bond_timestamp.seconds() { + first_bond_timestamp = bond.timestamp; + } - for bond in bonds { - if bond.timestamp.seconds() < first_bond_timestamp.seconds() { - first_bond_timestamp = bond.timestamp; + total_bonded = total_bonded.checked_add(bond.asset.amount)?; + bonded_assets.push(bond.asset); } - total_bonded = total_bonded.checked_add(bond.asset.amount)?; - bonded_assets.push(bond.asset); - } - // TODO: This is hardcoded, either we add to config the address of epoch manager and query - // or we store the genesis epoch itself in the bonding manager - // Query epoch manager for EpochConfig - let epoch_config: ConfigResponse = - deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "contract0".to_string(), - msg: to_json_binary(&QueryMsg::Config {})?, - }))?; - - let first_bonded_epoch_id = - helpers::calculate_epoch(epoch_config.epoch_config, first_bond_timestamp)?; + let config = CONFIG.load(deps.storage)?; + // Query epoch manager for EpochConfig + let epoch_config: ConfigResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: config.epoch_manager_addr.to_string(), + msg: to_json_binary(&QueryMsg::Config {})?, + }))?; + + let first_bonded_epoch_id = + helpers::calculate_epoch(epoch_config.epoch_config, first_bond_timestamp)?; + + (total_bonded, bonded_assets, Some(first_bonded_epoch_id)) + } else { + let global_index = GLOBAL.may_load(deps.storage)?.unwrap_or_default(); + (global_index.bonded_amount, global_index.bonded_assets, None) + }; Ok(BondedResponse { total_bonded, @@ -188,6 +196,7 @@ pub(crate) fn query_weight( total_bond_weight = total_bond_weight.checked_add(bond.weight)?; } + // If a global weight from an Epoch was passed, use that to get the weight, otherwise use the current global index weight let mut global_index = if let Some(global_index) = global_index { global_index } else { @@ -197,7 +206,6 @@ pub(crate) fn query_weight( .ok_or_else(|| StdError::generic_err("Global index not found"))? }; - // If a global weight from an Epoch was passed, use that to get the weight, otherwise use the current global index weight global_index.weight = get_weight( timestamp, global_index.weight, @@ -208,7 +216,11 @@ pub(crate) fn query_weight( // Represents the share of the global weight that the address has // If global_index.weight is zero no one has bonded yet so the share is - let share = Decimal::from_ratio(total_bond_weight, global_index.weight); + let share = if global_index.weight.is_zero() { + Decimal::zero() + } else { + Decimal::from_ratio(total_bond_weight, global_index.weight) + }; Ok(BondingWeightResponse { address: address.to_string(), @@ -225,7 +237,7 @@ pub fn query_total_bonded(deps: Deps) -> StdResult { Ok(BondedResponse { total_bonded: global_index.bonded_amount, bonded_assets: global_index.bonded_assets, - first_bonded_epoch_id: Default::default(), //ignore this parameter here + first_bonded_epoch_id: None, // N/A }) } @@ -235,44 +247,25 @@ pub fn query_global_index(deps: Deps) -> StdResult { Ok(global_index) } -/// Returns the current epoch, which is the last on the EPOCHS map. -pub fn get_current_epoch(deps: Deps) -> StdResult { - let option = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .next(); - - let epoch = match option { - Some(Ok((_, epoch))) => epoch, - _ => Epoch::default(), - }; - - Ok(EpochResponse { epoch }) -} - /// Returns the epoch that is falling out the grace period, which is the one expiring after creating /// a new epoch is created. pub fn get_expiring_epoch(deps: Deps) -> StdResult> { let config = CONFIG.load(deps.storage)?; // Adding 1 because we store the future epoch in the map also, so grace_period + 1 - let grace_period = config.grace_period.u64() + 1; + let grace_period_plus_future_epoch = config.grace_period.u64() + 1u64; // Take grace_period + 1 and then slice last one off - let mut epochs = EPOCHS + let epochs = EPOCHS .range(deps.storage, None, None, Order::Descending) - .take(grace_period as usize) + .take(grace_period_plus_future_epoch as usize) .map(|item| { let (_, epoch) = item?; Ok(epoch) }) - .collect::>>()?; - - if epochs.len() > 1 { - // First the future epoch from stack - epochs.pop_front(); - } + .collect::>>()?; // if the epochs vector's length is the same as the grace period it means there is one epoch that // is expiring once the new one is created i.e. the last epoch in the vector - if epochs.len() == config.grace_period.u64() as usize { + if epochs.len() == grace_period_plus_future_epoch as usize { let expiring_epoch: Epoch = epochs.into_iter().last().unwrap_or_default(); Ok(Some(expiring_epoch)) } else { @@ -298,44 +291,62 @@ pub fn get_claimable_epochs(deps: Deps) -> StdResult { }) .collect::>>()?; - if epochs.len() > 1 { - // First the future epoch from stack - epochs.pop_front(); - } + println!("epochs: {:?}", epochs); + + // Remove the upcoming epoch from stack + epochs.pop_front(); epochs.retain(|epoch| !epoch.available.is_empty()); + println!("epochs: {:?}", epochs); + Ok(ClaimableEpochsResponse { epochs: epochs.into(), }) } /// Returns the epochs that can be claimed by the given address. -pub fn query_claimable(deps: Deps, address: &Addr) -> StdResult { +pub fn query_claimable(deps: Deps, address: Option) -> StdResult { let mut claimable_epochs = get_claimable_epochs(deps)?.epochs; - let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, address)?; - // filter out epochs that have already been claimed by the user - if let Some(last_claimed_epoch) = last_claimed_epoch { - claimable_epochs.retain(|epoch| epoch.id > last_claimed_epoch); - } else { - // if the user doesn't have any last_claimed_epoch two things might be happening: - // 1- the user has never bonded before - // 2- the user has bonded, but never claimed any rewards so far + // if an address is provided, filter what's claimable for that address + if let Some(address) = address { + let address = deps.api.addr_validate(&address)?; - let bonded_response: BondedResponse = query_bonded(deps, address.to_string())?; + let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, &address)?; - if bonded_response.bonded_assets.is_empty() { - // the user has never bonded before, therefore it shouldn't be able to claim anything - claimable_epochs.clear(); + // filter out epochs that have already been claimed by the user + if let Some(last_claimed_epoch) = last_claimed_epoch { + claimable_epochs.retain(|epoch| epoch.id > last_claimed_epoch); } else { - // the user has bonded, but never claimed any rewards so far - claimable_epochs.retain(|epoch| epoch.id > bonded_response.first_bonded_epoch_id); - } - }; - // filter out epochs that have no available fees. This would only happen in case the grace period - // gets increased after epochs have expired, which would lead to make them available for claiming - // again without any available rewards, as those were forwarded to newer epochs. - claimable_epochs.retain(|epoch| !epoch.available.is_empty()); + // if the user doesn't have any last_claimed_epoch two things might be happening: + // 1- the user has never bonded before + // 2- the user has bonded, but never claimed any rewards so far + + let bonded_response: BondedResponse = query_bonded(deps, Some(address.into_string()))?; + println!("bonded_responsebonded_response: {:?}", bonded_response); + if bonded_response.bonded_assets.is_empty() { + // the user has never bonded before, therefore it shouldn't be able to claim anything + claimable_epochs.clear(); + } else { + // the user has bonded, but never claimed any rewards so far. The first_bonded_epoch_id + // value should always be Some here, as `query_bonded` will always return Some. + match bonded_response.first_bonded_epoch_id { + Some(first_bonded_epoch_id) => { + // keep all epochs that are newer than the first bonded epoch + claimable_epochs.retain(|epoch| epoch.id > first_bonded_epoch_id); + } + None => { + // for sanity, it should never happen + claimable_epochs.clear(); + } + } + } + }; + // filter out epochs that have no available fees. This would only happen in case the grace period + // gets increased after epochs have expired, which would lead to make them available for claiming + // again without any available rewards, as those were forwarded to newer epochs. + claimable_epochs.retain(|epoch| !epoch.available.is_empty()); + } Ok(ClaimableEpochsResponse { epochs: claimable_epochs, diff --git a/contracts/liquidity_hub/bonding-manager/src/state.rs b/contracts/liquidity_hub/bonding-manager/src/state.rs index 7331461c..6210e2fc 100644 --- a/contracts/liquidity_hub/bonding-manager/src/state.rs +++ b/contracts/liquidity_hub/bonding-manager/src/state.rs @@ -1,12 +1,9 @@ -use crate::queries::query_bonded; use crate::ContractError; use cosmwasm_std::{ Addr, Decimal, Deps, DepsMut, Order, StdError, StdResult, Timestamp, Uint128, Uint64, }; use cw_storage_plus::{Item, Map}; -use white_whale_std::bonding_manager::{ - Bond, BondedResponse, ClaimableEpochsResponse, Config, Epoch, EpochResponse, GlobalIndex, -}; +use white_whale_std::bonding_manager::{Bond, Config, Epoch, GlobalIndex}; type Denom = str; @@ -15,10 +12,6 @@ pub const CONFIG: Item = Item::new("config"); pub const BOND: Map<(&Addr, &Denom), Bond> = Map::new("bond"); pub const UNBOND: Map<(&Addr, &Denom, u64), Bond> = Map::new("unbond"); pub const GLOBAL: Item = Item::new("global"); -pub type EpochID = [u8]; - -pub const REWARDS_BUCKET: Map<&EpochID, &Epoch> = Map::new("rewards_bucket"); - pub const LAST_CLAIMED_EPOCH: Map<&Addr, Uint64> = Map::new("last_claimed_epoch"); pub const EPOCHS: Map<&[u8], Epoch> = Map::new("epochs"); @@ -43,6 +36,7 @@ pub fn update_local_weight( let denom: &String = &bond.asset.denom; + //todo remove? done outside of this function. Or remove outside BOND.save(deps.storage, (&address, denom), &bond)?; Ok(bond) @@ -66,6 +60,7 @@ pub fn update_global_weight( global_index.timestamp = timestamp; + //todo remove? done outside of this function. Or remove outside GLOBAL.save(deps.storage, &global_index)?; Ok(global_index) @@ -93,32 +88,6 @@ pub fn get_weight( Ok(weight.checked_add(amount.checked_mul(time_factor)? * growth_rate)?) } -/// Returns the current epoch, which is the last on the EPOCHS map. -pub fn get_current_epoch(deps: Deps) -> StdResult { - let option = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .next(); - - let epoch = match option { - Some(Ok((_, epoch))) => epoch, - _ => Epoch::default(), - }; - - Ok(EpochResponse { epoch }) -} - -/// Returns the [Epoch] with the given id. -pub fn get_epoch(deps: Deps, id: Uint64) -> StdResult { - let option = EPOCHS.may_load(deps.storage, &id.to_be_bytes())?; - - let epoch = match option { - Some(epoch) => epoch, - None => Epoch::default(), - }; - - Ok(EpochResponse { epoch }) -} - /// Returns the epoch that is falling out the grace period, which is the one expiring after creating /// a new epoch is created. pub fn get_expiring_epoch(deps: Deps) -> StdResult> { @@ -143,54 +112,3 @@ pub fn get_expiring_epoch(deps: Deps) -> StdResult> { Ok(None) } } - -/// Returns the epochs that are within the grace period, i.e. the ones which fees can still be claimed. -/// The result is ordered by epoch id, descending. Thus, the first element is the current epoch. -pub fn get_claimable_epochs(deps: Deps) -> StdResult { - let grace_period = CONFIG.load(deps.storage)?.grace_period; - - let epochs = EPOCHS - .range(deps.storage, None, None, Order::Descending) - .take(grace_period.u64() as usize) - .map(|item| { - let (_, epoch) = item?; - Ok(epoch) - }) - .collect::>>()?; - - Ok(ClaimableEpochsResponse { epochs }) -} - -/// Returns the epochs that can be claimed by the given address. -pub fn query_claimable(deps: Deps, address: &Addr) -> StdResult { - let mut claimable_epochs = get_claimable_epochs(deps)?.epochs; - let last_claimed_epoch = LAST_CLAIMED_EPOCH.may_load(deps.storage, address)?; - - // filter out epochs that have already been claimed by the user - if let Some(last_claimed_epoch) = last_claimed_epoch { - claimable_epochs.retain(|epoch| epoch.id > last_claimed_epoch); - } else { - // if the user doesn't have any last_claimed_epoch two things might be happening: - // 1- the user has never bonded before - // 2- the user has bonded, but never claimed any rewards so far - - let bonded_response: BondedResponse = query_bonded(deps, address.to_string())?; - - if bonded_response.bonded_assets.is_empty() { - // the user has never bonded before, therefore it shouldn't be able to claim anything - claimable_epochs.clear(); - } else { - // the user has bonded, but never claimed any rewards so far - claimable_epochs.retain(|epoch| epoch.id > bonded_response.first_bonded_epoch_id); - } - }; - - // filter out epochs that have no available fees. This would only happen in case the grace period - // gets increased after epochs have expired, which would lead to make them available for claiming - // again without any available rewards, as those were forwarded to newer epochs. - claimable_epochs.retain(|epoch| !epoch.available.is_empty()); - - Ok(ClaimableEpochsResponse { - epochs: claimable_epochs, - }) -} diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs index e4195559..a6eb7700 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/claim.rs @@ -8,7 +8,8 @@ use crate::tests::robot::TestingRobot; use crate::tests::test_helpers; use cosmwasm_std::{coins, Coin, Decimal, Timestamp, Uint128}; -use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse}; +use crate::ContractError; +use white_whale_std::bonding_manager::{BondedResponse, BondingWeightResponse, Epoch, GlobalIndex}; use super::test_helpers::get_epochs; @@ -16,26 +17,156 @@ use super::test_helpers::get_epochs; fn test_claimable_epochs() { let mut robot = TestingRobot::default(); let grace_period = Uint64::new(21); + let creator = robot.sender.clone(); - let epochs = test_helpers::get_epochs(); - let binding = epochs.clone(); - let mut claimable_epochs = binding - .iter() - .rev() - .take(grace_period.u64() as usize) - .collect::>(); - claimable_epochs.pop_front(); + let asset_denoms = vec!["uwhale".to_string(), "uusdc".to_string()]; + + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 100u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; robot .instantiate_default() - .add_epochs_to_state(epochs) - .query_claimable_epochs(None, |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), claimable_epochs.len()); - for (e, a) in epochs.iter().zip(claimable_epochs.iter()) { - assert_eq!(e, *a); - } - }); + .fast_forward(259_200) + .create_epoch(|result| { + result.unwrap(); + }) + .create_pair( + creator.clone(), + asset_denoms.clone(), + pool_fees.clone(), + white_whale_std::pool_manager::PoolType::ConstantProduct, + Some("whale-uusdc".to_string()), + vec![coin(1000, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uusdc".to_string(), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000_000u128), + }, + Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000_000_000u128), + }, + ], + |result| { + // Ensure we got 999_000 in the response which is 1mil less the initial liquidity amount + assert!(result.unwrap().events.iter().any(|event| { + event.attributes.iter().any(|attr| { + attr.key == "share" + && attr.value + == (Uint128::from(1_000_000_000u128) - MINIMUM_LIQUIDITY_AMOUNT) + .to_string() + }) + })); + }, + ) + .swap( + creator.clone(), + coin(1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + creator.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .create_epoch(|result| { + result.unwrap(); + }) + .swap( + creator.clone(), + coin(2_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(2_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + creator.clone(), + coin(2_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(2_000u128), + }], + |result| { + result.unwrap(); + }, + ); + + let expected_epochs = vec![Epoch { + id: Uint64::new(2u64), + start_time: Timestamp::from_nanos(1571970229879305533), + total: vec![coin(1009u128, "uwhale")], + available: vec![coin(1009u128, "uwhale")], + claimed: vec![], + global_index: GlobalIndex { + bonded_amount: Default::default(), + bonded_assets: vec![], + timestamp: Timestamp::from_nanos(1572056619879305533), + weight: Default::default(), + }, + }]; + + robot.query_claimable_epochs(None, |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), expected_epochs.len()); + + for (index, epoch) in epochs.iter().enumerate() { + assert_eq!(expected_epochs[index], epoch.clone()); + } + }); } #[test] @@ -60,7 +191,7 @@ fn test_claim_successfully() { }, extra_fees: vec![], }; - get_epochs(); + robot .instantiate_default() .bond( @@ -83,18 +214,20 @@ fn test_claim_successfully() { first_bonded_epoch_id: Default::default(), }, ) - .fast_forward(10u64) + .fast_forward(100_000u64) + .create_epoch(|result| { + result.unwrap(); + }) .assert_bonding_weight_response( sender.to_string(), BondingWeightResponse { address: sender.to_string(), - weight: Uint128::new(11_000u128), - global_weight: Uint128::new(11_000u128), + weight: Uint128::new(100_001_000u128), + global_weight: Uint128::new(100_001_000u128), share: Decimal::one(), - timestamp: Timestamp::from_nanos(1571797429879305533u64), + timestamp: Timestamp::from_nanos(1571897419879305533u64), }, ) - .fast_forward(10u64) .bond( sender.clone(), Coin { @@ -121,47 +254,16 @@ fn test_claim_successfully() { first_bonded_epoch_id: Default::default(), }, ) - .fast_forward(10u64) - .bond( - another_sender.clone(), - Coin { - denom: "ampWHALE".to_string(), - amount: Uint128::new(5_000u128), - }, - &coins(5_000u128, "ampWHALE"), - |_res| {}, - ) - .fast_forward(10u64) - .assert_bonding_weight_response( - sender.to_string(), - BondingWeightResponse { - address: sender.to_string(), - weight: Uint128::new(104_000u128), - global_weight: Uint128::new(269_000u128), - share: Decimal::from_ratio(104_000u128, 269_000u128), - timestamp: Timestamp::from_nanos(1571797459879305533u64), - }, - ) - .assert_bonding_weight_response( - another_sender.to_string(), - BondingWeightResponse { - address: another_sender.to_string(), - weight: Uint128::new(55_000u128), - global_weight: Uint128::new(269_000u128), - share: Decimal::from_ratio(55_000u128, 269_000u128), - timestamp: Timestamp::from_nanos(1571797459879305533u64), - }, - ) .query_total_bonded(|res| { let bonded_response = res.unwrap().1; assert_eq!( bonded_response, BondedResponse { - total_bonded: Uint128::new(9_000u128), + total_bonded: Uint128::new(4_000u128), bonded_assets: vec![ Coin { denom: "ampWHALE".to_string(), - amount: Uint128::new(6_000u128), + amount: Uint128::new(1_000u128), }, Coin { denom: "bWHALE".to_string(), @@ -178,86 +280,239 @@ fn test_claim_successfully() { assert_eq!(epochs.len(), 0); }); - robot.create_pair( - sender.clone(), - asset_infos.clone(), - pool_fees.clone(), - white_whale_std::pool_manager::PoolType::ConstantProduct, - Some("whale-uusdc".to_string()), - vec![coin(1000, "uwhale")], - |result| { - result.unwrap(); - }, - ); - - // Lets try to add liquidity - robot.provide_liquidity( - sender.clone(), - "whale-uusdc".to_string(), - vec![ - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1000000000u128), + println!("-------"); + robot + .create_pair( + sender.clone(), + asset_infos.clone(), + pool_fees.clone(), + white_whale_std::pool_manager::PoolType::ConstantProduct, + Some("whale-uusdc".to_string()), + vec![coin(1000, "uwhale")], + |result| { + result.unwrap(); }, - Coin { + ) + .provide_liquidity( + sender.clone(), + "whale-uusdc".to_string(), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1000000000u128), + }, + Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1000000000u128), + }, + ], + |result| { + // Ensure we got 999_000 in the response which is 1mil less the initial liquidity amount + assert!(result.unwrap().events.iter().any(|event| { + event.attributes.iter().any(|attr| { + attr.key == "share" + && attr.value + == (Uint128::from(1000000000u128) - MINIMUM_LIQUIDITY_AMOUNT) + .to_string() + }) + })); + }, + ) + .swap( + sender.clone(), + coin(1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { denom: "uusdc".to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); }, - ], - |result| { - // Ensure we got 999_000 in the response which is 1mil less the initial liquidity amount - assert!(result.unwrap().events.iter().any(|event| { - event.attributes.iter().any(|attr| { - attr.key == "share" - && attr.value - == (Uint128::from(1000000000u128) - MINIMUM_LIQUIDITY_AMOUNT) - .to_string() - }) - })); - }, - ); - - robot.swap( - sender.clone(), - coin(1_000u128, "uusdc"), - "uwhale".to_string(), - None, - None, - None, - "whale-uusdc".to_string(), - vec![Coin { - denom: "uusdc".to_string(), - amount: Uint128::from(1_000u128), - }], - |result| { + ) + .swap( + sender.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .fast_forward(90_000) + .create_epoch(|result| { result.unwrap(); - }, - ); + }) + .swap( + sender.clone(), + coin(1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + sender.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ); - robot - .create_new_epoch() - .query_claimable_epochs_live(Some(sender.clone()), |res| { - let (_, epochs) = res.unwrap(); - assert_eq!(epochs.len(), 1); - }); + robot.query_claimable_epochs_live(None, |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), 1); + }); + robot.query_claimable_epochs_live(Some(sender.clone()), |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), 1); + }); - robot.claim(sender, |res| { + robot.claim(sender.clone(), |res| { let result = res.unwrap(); assert!(result.events.iter().any(|event| { event .attributes .iter() - .any(|attr| attr.key == "amount" && attr.value == "448uwhale") + .any(|attr| attr.key == "amount" && attr.value == "571uwhale") })); }); - robot.claim(another_sender, |res| { - let result = res.unwrap(); - assert!(result.events.iter().any(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "amount" && attr.value == "560uwhale") - })); + robot.claim(another_sender.clone(), |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::NothingToClaim => {} + _ => { + panic!("Wrong error type, should return ContractError::NothingToClaim") + } + } + }); + + robot + .bond( + another_sender.clone(), + Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(9_000u128), + }, + &coins(9_000u128, "ampWHALE"), + |res| { + res.unwrap(); + }, + ) + .assert_bonded_response( + another_sender.to_string(), + BondedResponse { + total_bonded: Uint128::new(9_000u128), + bonded_assets: vec![Coin { + denom: "ampWHALE".to_string(), + amount: Uint128::new(9_000u128), + }], + first_bonded_epoch_id: Uint64::new(3u64), + }, + ); + + robot + .fast_forward(100_000) + .create_epoch(|result| { + result.unwrap(); + println!("*****"); + }) + .swap( + another_sender.clone(), + coin(1_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + another_sender.clone(), + coin(1_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + sender.clone(), + coin(5_000u128, "uusdc"), + "uwhale".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uusdc".to_string(), + amount: Uint128::from(5_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .swap( + sender.clone(), + coin(5_000u128, "uwhale"), + "uusdc".to_string(), + None, + None, + None, + "whale-uusdc".to_string(), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(5_000u128), + }], + |result| { + result.unwrap(); + }, + ); + + robot.query_claimable_epochs_live(Some(another_sender.clone()), |res| { + let (_, epochs) = res.unwrap(); + assert_eq!(epochs.len(), 1); }); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs index 91166618..0ad6aa84 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/instantiate.rs @@ -1,6 +1,8 @@ use cosmwasm_std::{Addr, Decimal, Uint64}; +use crate::state::BONDING_ASSETS_LIMIT; use crate::tests::robot::TestingRobot; +use crate::ContractError; use white_whale_std::bonding_manager::Config; #[test] @@ -15,7 +17,7 @@ fn test_instantiate_successfully() { &vec![], ) .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000u64), @@ -40,12 +42,10 @@ fn test_instantiate_unsuccessfully() { ], &vec![], |error| { - println!("1 --{error:?}"); - println!("2 --{:?}", error.root_cause()); - //println!("3 --{:?}", error.root_cause().downcast_ref::()); - // assert_eq!( - // error.root_cause().downcast_ref::().unwrap(), - // &ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, 3)); + assert_eq!( + error.root_cause().downcast_ref::().unwrap(), + &ContractError::InvalidBondingAssetsLimit(BONDING_ASSETS_LIMIT, 3) + ); }, ); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs index b05b9f91..143289a2 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/rewards.rs @@ -12,9 +12,8 @@ use crate::tests::test_helpers; fn test_fill_rewards_from_pool_manager() { let mut robot = TestingRobot::default(); let creator = robot.sender.clone(); - let epochs = test_helpers::get_epochs(); - let asset_infos = vec!["uwhale".to_string(), "uusdc".to_string()]; + let asset_denoms = vec!["uwhale".to_string(), "uusdc".to_string()]; // Default Pool fees white_whale_std::pool_network::pair::PoolFee // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% @@ -34,10 +33,13 @@ fn test_fill_rewards_from_pool_manager() { robot .instantiate_default() - .add_epochs_to_state(epochs) + .fast_forward(90_000) + .create_epoch(|result| { + result.unwrap(); + }) .create_pair( creator.clone(), - asset_infos.clone(), + asset_denoms.clone(), pool_fees.clone(), white_whale_std::pool_manager::PoolType::ConstantProduct, Some("whale-uusdc".to_string()), @@ -54,29 +56,26 @@ fn test_fill_rewards_from_pool_manager() { vec![ Coin { denom: "uwhale".to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(1_000_000_000u128), }, Coin { denom: "uusdc".to_string(), - amount: Uint128::from(1000000000u128), + amount: Uint128::from(1_000_000_000u128), }, ], |result| { - println!("{:?}", result.as_ref().unwrap()); // Ensure we got 999_000 in the response which is 1mil less the initial liquidity amount assert!(result.unwrap().events.iter().any(|event| { event.attributes.iter().any(|attr| { attr.key == "share" && attr.value - == (Uint128::from(1000000000u128) - MINIMUM_LIQUIDITY_AMOUNT) + == (Uint128::from(1_000_000_000u128) - MINIMUM_LIQUIDITY_AMOUNT) .to_string() }) })); }, ); - println!("{:?}", robot.app.wrap().query_all_balances(creator.clone())); - // Lets try to add a swap route let swap_route_1 = SwapRoute { offer_asset_denom: "uusdc".to_string(), @@ -88,7 +87,7 @@ fn test_fill_rewards_from_pool_manager() { }], }; robot.add_swap_routes(creator.clone(), vec![swap_route_1], |res| { - println!("{:?}", res.unwrap()); + res.unwrap(); }); robot.swap( @@ -121,7 +120,7 @@ fn test_fill_rewards_from_pool_manager() { robot.create_pair( creator.clone(), - asset_infos.clone(), + asset_denoms.clone(), pool_fees.clone(), white_whale_std::pool_manager::PoolType::ConstantProduct, Some("whale-uusdc-second".to_string()), @@ -143,7 +142,7 @@ fn test_fill_rewards_from_pool_manager() { // create another pair to collect another fee robot.create_pair( creator.clone(), - asset_infos, + asset_denoms, pool_fees, white_whale_std::pool_manager::PoolType::ConstantProduct, Some("whale-uusdc-third".to_string()), @@ -168,7 +167,47 @@ fn test_fill_rewards_from_pool_manager() { "factory/contract2/uwhale-uusdc.pool.whale-uusdc.uLP", )], |res| { - println!("{:?}", res.unwrap()); + res.unwrap(); + }, + ); + + let bonding_manager_addr = robot.bonding_manager_addr.clone(); + let bonding_manager_balances = robot + .app + .wrap() + .query_all_balances(bonding_manager_addr.clone()) + .unwrap(); + assert_eq!(bonding_manager_balances.len(), 1); + assert_eq!(bonding_manager_balances[0].amount, Uint128::from(4998u128)); + + // send some random asset that doesn't have swap routes + robot.fill_rewards_lp( + creator.clone(), + vec![coin(1000, "non_whitelisted_asset")], + |res| { + res.unwrap(); }, ); + + let bonding_manager_addr = robot.bonding_manager_addr.clone(); + let bonding_manager_balances = robot + .app + .wrap() + .query_all_balances(bonding_manager_addr.clone()) + .unwrap(); + assert_eq!(bonding_manager_balances.len(), 2); + assert_eq!( + bonding_manager_balances, + vec![ + // wasn't swapped + Coin { + denom: "non_whitelisted_asset".to_string(), + amount: Uint128::from(1000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(4998u128), + }, + ] + ); } diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs index d2e422a8..7f848598 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/robot.rs @@ -1,7 +1,7 @@ use anyhow::Error; use cosmwasm_std::testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{ - coin, from_json, Addr, Coin, Decimal, Empty, OwnedDeps, StdResult, Uint128, Uint64, + coin, from_json, Addr, Binary, Coin, Decimal, Empty, OwnedDeps, StdResult, Uint128, Uint64, }; // use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; use cw_multi_test::{ @@ -84,7 +84,7 @@ pub struct TestingRobot { /// instantiate / execute messages impl TestingRobot { pub(crate) fn default() -> Self { - let sender = Addr::unchecked("owner"); + let sender = Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"); let another_sender = Addr::unchecked("migaloo193lk767456jhkzddnz7kf5jvuzfn67gyfvhc40"); let sender_3 = Addr::unchecked("migaloo1ludaslnu24p5eftw499f7ngsc2jkzqdsrvxt75"); @@ -196,16 +196,6 @@ impl TestingRobot { .unwrap(); println!("hook_registration_msg: {:?}", resp); - // self.fast_forward(10); - let new_epoch_msg = white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch; - self.app - .execute_contract( - self.sender.clone(), - epoch_manager_addr.clone(), - &new_epoch_msg, - &[], - ) - .unwrap(); let msg = white_whale_std::pool_manager::InstantiateMsg { bonding_manager_addr: bonding_manager_addr.clone().to_string(), @@ -240,6 +230,7 @@ impl TestingRobot { self.app .execute_contract(self.sender.clone(), bonding_manager_addr.clone(), &msg, &[]) .unwrap(); + self.bonding_manager_addr = bonding_manager_addr; self.pool_manager_addr = pool_manager_addr; self.epoch_manager_addr = epoch_manager_addr; @@ -387,20 +378,6 @@ impl TestingRobot { ) .unwrap(); } - CONFIG - .save( - &mut self.owned_deps.storage, - &Config { - distribution_denom: "uwhale".to_string(), - unbonding_period: Uint64::new(1_000_000_000_000u64), - growth_rate: Decimal::one(), - bonding_assets: vec!["ampWHALE".to_string(), "bWHALE".to_string()], - grace_period: Uint64::new(21), - owner: Addr::unchecked("owner"), - pool_manager_addr: Addr::unchecked("pool_manager"), - }, - ) - .unwrap(); self } @@ -427,7 +404,7 @@ fn instantiate_contract( robot.sender.clone(), &msg, funds, - "White Whale Lair".to_string(), + "Bonding Manager".to_string(), Some(robot.sender.clone().to_string()), ) } @@ -478,27 +455,24 @@ impl TestingRobot { address: Option, response: impl Fn(StdResult<(&mut Self, Vec)>), ) -> &mut Self { - let query_res = if let Some(address) = address { - query( - self.owned_deps.as_ref(), - self.env.clone(), - QueryMsg::Claimable { - addr: address.to_string(), - }, - ) - .unwrap() + let query_res: ClaimableEpochsResponse = if let Some(address) = address { + self.app + .wrap() + .query_wasm_smart( + &self.bonding_manager_addr, + &QueryMsg::Claimable { + addr: address.to_string(), + }, + ) + .unwrap() } else { - query( - self.owned_deps.as_ref(), - self.env.clone(), - QueryMsg::ClaimableEpochs {}, - ) - .unwrap() + self.app + .wrap() + .query_wasm_smart(&self.bonding_manager_addr, &QueryMsg::ClaimableEpochs {}) + .unwrap() }; - let res: ClaimableEpochsResponse = from_json(query_res).unwrap(); - - response(Ok((self, res.epochs))); + response(Ok((self, query_res.epochs))); self } @@ -731,6 +705,23 @@ impl TestingRobot { self } + + #[track_caller] + pub(crate) fn create_epoch( + &mut self, + result: impl Fn(Result), + ) -> &mut Self { + let sender = self.another_sender.clone(); + + result(self.app.execute_contract( + sender, + self.epoch_manager_addr.clone(), + &white_whale_std::epoch_manager::epoch_manager::ExecuteMsg::CreateEpoch, + &[], + )); + + self + } } /// assertions diff --git a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs index 96f586cc..a655db2e 100644 --- a/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs +++ b/contracts/liquidity_hub/bonding-manager/src/tests/update_config.rs @@ -12,7 +12,7 @@ fn test_update_config_successfully() { robot .instantiate_default() .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), @@ -66,7 +66,7 @@ fn test_update_config_unsuccessfully() { robot .instantiate_default() .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), @@ -92,7 +92,7 @@ fn test_update_config_unsuccessfully() { }, ) .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), @@ -118,7 +118,7 @@ fn test_update_config_unsuccessfully() { }, ) .assert_config(Config { - owner: Addr::unchecked("owner"), + owner: Addr::unchecked("migaloo1h3s5np57a8cxaca3rdjlgu8jzmr2d2zz55s5y3"), pool_manager_addr: Addr::unchecked("contract2"), distribution_denom: "uwhale".to_string(), unbonding_period: Uint64::new(1_000_000_000_000u64), diff --git a/contracts/liquidity_hub/epoch-manager/src/commands.rs b/contracts/liquidity_hub/epoch-manager/src/commands.rs index ef5f4b09..61fcb26f 100644 --- a/contracts/liquidity_hub/epoch-manager/src/commands.rs +++ b/contracts/liquidity_hub/epoch-manager/src/commands.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Api, DepsMut, Env, MessageInfo, Response, SubMsg}; +use cosmwasm_std::{ensure, Api, DepsMut, Env, MessageInfo, Response, SubMsg}; use white_whale_std::epoch_manager::epoch_manager::EpochConfig; use white_whale_std::epoch_manager::hooks::EpochChangedHookMsg; @@ -35,11 +35,16 @@ pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result= current_epoch.start_time, + ContractError::GenesisEpochHasNotStarted ); if env @@ -51,6 +56,7 @@ pub fn create_epoch(deps: DepsMut, env: Env, info: MessageInfo) -> Result= env.block.time, + ContractError::InvalidStartTime + ); - if msg.epoch_config.genesis_epoch.u64() != msg.start_epoch.start_time.nanos() { - return Err(ContractError::EpochConfigMismatch); - } + ensure!( + msg.epoch_config.genesis_epoch.u64() == msg.start_epoch.start_time.nanos(), + ContractError::EpochConfigMismatch + ); ADMIN.set(deps.branch(), Some(info.sender))?; EPOCHS.save(deps.storage, msg.start_epoch.id, &msg.start_epoch)?; diff --git a/contracts/liquidity_hub/epoch-manager/src/error.rs b/contracts/liquidity_hub/epoch-manager/src/error.rs index bb6e61aa..25c7d97b 100644 --- a/contracts/liquidity_hub/epoch-manager/src/error.rs +++ b/contracts/liquidity_hub/epoch-manager/src/error.rs @@ -33,6 +33,9 @@ pub enum ContractError { #[error("The current epoch epoch has not expired yet.")] CurrentEpochNotExpired, + #[error("The genesis epoch has not started yet.")] + GenesisEpochHasNotStarted, + #[error("start_time must be in the future.")] InvalidStartTime, diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index d8ba8036..ca76e0b7 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -112,9 +112,6 @@ pub enum ContractError { #[error("An overflow occurred when attempting to construct a decimal")] DecimalOverflow, - #[error("The token factory feature is not enabled")] - TokenFactoryNotEnabled, - #[error("{0}")] OverflowError(#[from] OverflowError), diff --git a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs index 9c19b832..0c679f11 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -174,15 +174,6 @@ pub fn create_pool( attributes.push(attr("lp_asset", lp_asset)); - #[cfg(all( - not(feature = "token_factory"), - not(feature = "osmosis_token_factory"), - not(feature = "injective") - ))] - { - return Err(ContractError::TokenFactoryNotEnabled); - } - messages.push(white_whale_std::tokenfactory::create_denom::create_denom( env.contract.address, lp_symbol, diff --git a/contracts/liquidity_hub/pool-manager/src/queries.rs b/contracts/liquidity_hub/pool-manager/src/queries.rs index b76533d3..330226ad 100644 --- a/contracts/liquidity_hub/pool-manager/src/queries.rs +++ b/contracts/liquidity_hub/pool-manager/src/queries.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use cosmwasm_std::{coin, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128}; +use cosmwasm_std::{coin, ensure, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128}; use white_whale_std::pool_manager::{ AssetDecimalsResponse, Config, PoolInfoResponse, PoolType, ReverseSimulationResponse, @@ -302,9 +302,7 @@ pub fn simulate_swap_operations( operations: Vec, ) -> Result { let operations_len = operations.len(); - if operations_len == 0 { - return Err(ContractError::NoSwapOperationsProvided); - } + ensure!(operations_len > 0, ContractError::NoSwapOperationsProvided); let mut amount = offer_amount; diff --git a/packages/white-whale-std/src/bonding_manager.rs b/packages/white-whale-std/src/bonding_manager.rs index cfa5a9e7..f075c086 100644 --- a/packages/white-whale-std/src/bonding_manager.rs +++ b/packages/white-whale-std/src/bonding_manager.rs @@ -4,16 +4,18 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ to_json_binary, Addr, Coin, CosmosMsg, Decimal, StdResult, Timestamp, Uint128, Uint64, WasmMsg, }; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; #[cw_serde] pub struct Config { - /// Owner of the contract. - pub owner: Addr, /// Pool Manager contract address for swapping pub pool_manager_addr: Addr, + /// Epoch Manager contract address + pub epoch_manager_addr: Addr, /// Distribution denom for the rewards pub distribution_denom: String, - /// Unbonding period in nanoseconds. + /// Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can + /// be withdrawn pub unbonding_period: Uint64, /// A fraction that controls the effect of time on the weight of a bond. If the growth rate is set /// to zero, time will have no impact on the weight. @@ -81,7 +83,8 @@ pub struct GlobalIndex { pub struct InstantiateMsg { /// Denom to be swapped to and rewarded pub distribution_denom: String, - /// Unbonding period in nanoseconds. + /// Unbonding period in nanoseconds. The time that needs to pass before an unbonded position can + /// be withdrawn pub unbonding_period: Uint64, /// Weight grow rate. Needs to be between 0 and 1. pub growth_rate: Decimal, @@ -89,6 +92,8 @@ pub struct InstantiateMsg { pub bonding_assets: Vec, /// Grace period the maximum age of a bucket before fees are forwarded from it pub grace_period: Uint64, + /// The epoch manager contract + pub epoch_manager_addr: String, } #[cw_serde] @@ -96,85 +101,110 @@ pub struct EpochChangedHookMsg { pub current_epoch: EpochV2, } +#[cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { /// Bonds the specified [Asset]. - Bond {}, + Bond, /// Unbonds the specified [Asset]. Unbond { + /// The asset to unbond. asset: Coin, }, /// Sends withdrawable unbonded tokens to the user. Withdraw { + /// The denom to withdraw. denom: String, }, /// Updates the [Config] of the contract. UpdateConfig { - owner: Option, + /// The new pool manager address. pool_manager_addr: Option, + /// The unbonding period. unbonding_period: Option, + /// The new growth rate. growth_rate: Option, }, - Claim {}, + /// Claims the available rewards + Claim, - /// Fills the whale lair with new rewards. + /// Fills the contract with new rewards. FillRewards, - /// Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in the next epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) + /// Creates a new bucket for the rewards flowing from this time on, i.e. to be distributed in + /// the upcoming epoch. Also, forwards the expiring epoch (only 21 epochs are live at a given moment) EpochChangedHook { + /// The current epoch, the one that was newly created. current_epoch: EpochV2, }, } +#[cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { /// Returns the [Config] of te contract. #[returns(Config)] - Config {}, + Config, /// Returns the amount of assets that have been bonded by the specified address. #[returns(BondedResponse)] - Bonded { address: String }, + Bonded { + /// The address to check for bonded assets. If none is provided, all bonded assets in the + /// contract are returned. + address: Option, + }, /// Returns the amount of tokens of the given denom that are been unbonded by the specified address. /// Allows pagination with start_after and limit. #[returns(UnbondingResponse)] Unbonding { + /// The address to check for unbonding assets. address: String, + /// The denom to check for unbonding assets. denom: String, + /// The amount of unbonding assets to skip. Allows pagination. start_after: Option, + /// The maximum amount of unbonding assets to return. limit: Option, }, /// Returns the amount of unbonding tokens of the given denom for the specified address that can /// be withdrawn, i.e. that have passed the unbonding period. #[returns(WithdrawableResponse)] - Withdrawable { address: String, denom: String }, + Withdrawable { + /// The address to check for withdrawable assets. + address: String, + /// The denom to check for withdrawable assets. + denom: String, + }, /// Returns the weight of the address. #[returns(BondingWeightResponse)] Weight { + /// The address to check for weight. address: String, + /// The timestamp to check for weight. If none is provided, the current block time is used. timestamp: Option, + /// The global index to check for weight. If none is provided, the current global index is used. global_index: Option, }, /// Returns the total amount of assets that have been bonded to the contract. #[returns(BondedResponse)] - TotalBonded {}, + TotalBonded, /// Returns the global index of the contract. #[returns(GlobalIndex)] - GlobalIndex {}, - - /// Returns the [Epoch]s that can be claimed. - #[returns(ClaimableEpochsResponse)] - ClaimableEpochs {}, + GlobalIndex, /// Returns the [Epoch]s that can be claimed by an address. #[returns(ClaimableEpochsResponse)] - Claimable { addr: String }, + Claimable { + /// The address to check for claimable epochs. If none is provided, all possible epochs + /// stored in the contract that can potentially be claimed are returned. + address: Option, + }, } #[cw_serde] @@ -183,31 +213,44 @@ pub struct MigrateMsg {} /// Response for the Bonded query #[cw_serde] pub struct BondedResponse { + /// The total amount of bonded tokens by the address. Bear in mind the bonded assets are + /// considered to be equal for this purpose. pub total_bonded: Uint128, + /// The total amount of bonded assets by the address. pub bonded_assets: Vec, - pub first_bonded_epoch_id: Uint64, + /// If Some, the epoch id at which the user/address bonded first time. None is used when this + /// Response is used to check the bonded assets in the contract. + pub first_bonded_epoch_id: Option, } /// Response for the Unbonding query #[cw_serde] pub struct UnbondingResponse { + /// The total amount of unbonded tokens by the address. pub total_amount: Uint128, + /// The total amount of unbonded assets by the address. pub unbonding_requests: Vec, } /// Response for the Withdrawable query #[cw_serde] pub struct WithdrawableResponse { + /// The total amount of withdrawable assets by the address. pub withdrawable_amount: Uint128, } /// Response for the Weight query. #[cw_serde] pub struct BondingWeightResponse { + /// The weight of the address. pub address: String, + /// The weight of the address at the given timestamp. pub weight: Uint128, + /// The global weight of the contract. pub global_weight: Uint128, + /// The share the address has of the rewards at the particular timestamp. pub share: Decimal, + /// The timestamp at which the weight was calculated. pub timestamp: Timestamp, } @@ -220,12 +263,8 @@ pub fn fill_rewards_msg(contract_addr: String, assets: Vec) -> StdResult, } diff --git a/packages/white-whale-std/src/constants.rs b/packages/white-whale-std/src/constants.rs index c8b46adb..45a14c1c 100644 --- a/packages/white-whale-std/src/constants.rs +++ b/packages/white-whale-std/src/constants.rs @@ -1,2 +1,2 @@ pub const LP_SYMBOL: &str = "uLP"; -pub const DAY_SECONDS: u64 = 86400u64; +pub const DAY_IN_SECONDS: u64 = 86_400u64; diff --git a/packages/white-whale-std/src/epoch_manager/common.rs b/packages/white-whale-std/src/epoch_manager/common.rs index 7e065d10..ce067db2 100644 --- a/packages/white-whale-std/src/epoch_manager/common.rs +++ b/packages/white-whale-std/src/epoch_manager/common.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Deps, StdError, StdResult, Timestamp}; -use crate::constants::DAY_SECONDS; +use crate::constants::DAY_IN_SECONDS; use crate::epoch_manager::epoch_manager::{Epoch, EpochResponse, QueryMsg}; /// Queries the current epoch from the epoch manager contract @@ -17,7 +17,7 @@ pub fn validate_epoch(epoch: &Epoch, current_time: Timestamp) -> StdResult<()> { if current_time .minus_seconds(epoch.start_time.seconds()) .seconds() - < DAY_SECONDS + < DAY_IN_SECONDS { return Err(StdError::generic_err( "Current epoch has expired, please wait for the next epoch to start.",