diff --git a/Cargo.lock b/Cargo.lock index 5a24a722a..52cf94db9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10729,6 +10729,7 @@ version = "0.2.0" dependencies = [ "cosmwasm-schema 2.2.1", "cosmwasm-std 2.2.1", + "serde", ] [[package]] @@ -10924,6 +10925,24 @@ dependencies = [ "valence-storage-account", ] +[[package]] +name = "valence-nolus-lending" +version = "0.2.0" +dependencies = [ + "cosmwasm-schema 2.2.1", + "cosmwasm-std 2.2.1", + "cw-ownable", + "cw-storage-plus 2.0.0", + "schemars", + "serde", + "thiserror 1.0.69", + "valence-account-utils", + "valence-lending-utils", + "valence-library-base", + "valence-library-utils", + "valence-macros", +] + [[package]] name = "valence-osmosis-cl-lper" version = "0.2.0" @@ -11094,6 +11113,7 @@ dependencies = [ "valence-macros", "valence-mars-lending", "valence-neutron-ibc-transfer-library", + "valence-nolus-lending", "valence-osmosis-cl-lper", "valence-osmosis-cl-withdrawer", "valence-osmosis-gamm-lper", diff --git a/Cargo.toml b/Cargo.toml index aa8a99cf7..6e9556101 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,21 @@ [workspace] members = [ - "contracts/accounts/*", - "contracts/libraries/*", - "contracts/encoders/*", - "contracts/authorization", - "contracts/processor", - "contracts/testing/*", - "contracts/program-registry", - "contracts/middleware/type-registries/osmosis/osmo-26-0-0", - "contracts/middleware/broker", - "contracts/middleware/asserter", - "contracts/middleware/verification-gateway", - "packages/*", - "program-manager", - "e2e", - "examples/*", - "deployment/scripts/*", + "contracts/accounts/*", + "contracts/libraries/*", + "contracts/encoders/*", + "contracts/authorization", + "contracts/processor", + "contracts/testing/*", + "contracts/program-registry", + "contracts/middleware/type-registries/osmosis/osmo-26-0-0", + "contracts/middleware/broker", + "contracts/middleware/asserter", + "contracts/middleware/verification-gateway", + "packages/*", + "program-manager", + "e2e", + "examples/*", + "deployment/scripts/*", ] resolver = "2" @@ -95,11 +95,12 @@ valence-clearing-queue = { path = "contracts/libraries/clearing-qu # middleware valence-middleware-osmosis = { path = "contracts/middleware/type-registries/osmosis/osmo-26-0-0", features = [ - "library", + "library", ] } valence-middleware-broker = { path = "contracts/middleware/broker", features = ["library"] } valence-middleware-asserter = { path = "contracts/middleware/asserter", features = ["library"] } valence-mars-lending = { path = "contracts/libraries/mars-lending", features = ["library"] } +valence-nolus-lending = { path = "contracts/libraries/nolus-lending", features = ["library"] } # our packages valence-account-utils = { path = "packages/account-utils" } diff --git a/contracts/libraries/nolus-lending/.cargo/config.toml b/contracts/libraries/nolus-lending/.cargo/config.toml new file mode 100644 index 000000000..e03b96d17 --- /dev/null +++ b/contracts/libraries/nolus-lending/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/libraries/nolus-lending/Cargo.toml b/contracts/libraries/nolus-lending/Cargo.toml new file mode 100644 index 000000000..380d1e001 --- /dev/null +++ b/contracts/libraries/nolus-lending/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "valence-nolus-lending" +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true +repository.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +valence-macros = { workspace = true } +valence-library-utils = { workspace = true } +valence-library-base = { workspace = true } +valence-lending-utils = { workspace = true } +valence-account-utils = { workspace = true } diff --git a/contracts/libraries/nolus-lending/README.md b/contracts/libraries/nolus-lending/README.md new file mode 100644 index 000000000..38f9b62a0 --- /dev/null +++ b/contracts/libraries/nolus-lending/README.md @@ -0,0 +1,20 @@ +# Nolus Lending library + +The **Valence Nolus Lending** library enables lending on Nolus from a **input account**. Also, the library allows **withdrawing lent assets** from the **input account** on Nolus and depositing the withdrawed tokens into an **output account**. + +## Configuration + +The library is configured on instantiation via the `LibraryConfig` type. + +```rust +pub struct LibraryConfig { + // Address of the input account + pub input_addr: LibraryAccountType, + /// Address of the output account + pub output_addr: LibraryAccountType, + // Address of the pool contract + pub pool_addr: String, + // Denom of the asset we are going to lend + pub denom: String, +} +``` diff --git a/contracts/libraries/nolus-lending/schema/valence-nolus-lending.json b/contracts/libraries/nolus-lending/schema/valence-nolus-lending.json new file mode 100644 index 000000000..e6fb1f9e0 --- /dev/null +++ b/contracts/libraries/nolus-lending/schema/valence-nolus-lending.json @@ -0,0 +1,696 @@ +{ + "contract_name": "valence-nolus-lending", + "contract_version": "0.2.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "config", + "owner", + "processor" + ], + "properties": { + "config": { + "$ref": "#/definitions/LibraryConfig" + }, + "owner": { + "type": "string" + }, + "processor": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "LibraryAccountType": { + "description": "A helper type that is used to associate an account or library with an id When a program is not instantiated yet, ids will be used to reference accounts and libraries When a program is instantiated, the ids will be replaced by the instantiated addresses", + "oneOf": [ + { + "type": "object", + "required": [ + "|library_account_addr|" + ], + "properties": { + "|library_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|library_id|" + ], + "properties": { + "|library_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "LibraryConfig": { + "type": "object", + "required": [ + "denom", + "input_addr", + "output_addr", + "pool_addr" + ], + "properties": { + "denom": { + "type": "string" + }, + "input_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "output_addr": { + "description": "Address of the output account", + "allOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + } + ] + }, + "pool_addr": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "process_function" + ], + "properties": { + "process_function": { + "$ref": "#/definitions/FunctionMsgs" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "new_config" + ], + "properties": { + "new_config": { + "$ref": "#/definitions/LibraryConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_processor" + ], + "properties": { + "update_processor": { + "type": "object", + "required": [ + "processor" + ], + "properties": { + "processor": { + "type": "string" + } + }, + "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" + ] + } + ] + }, + "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 + } + ] + }, + "FunctionMsgs": { + "oneOf": [ + { + "description": "Message to lend tokens.", + "type": "object", + "required": [ + "lend" + ], + "properties": { + "lend": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Message to withdraw tokens. If amount is not specified, full amount will be withdrawn.", + "type": "object", + "required": [ + "withdraw" + ], + "properties": { + "withdraw": { + "type": "object", + "properties": { + "amount": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "LibraryAccountType": { + "description": "A helper type that is used to associate an account or library with an id When a program is not instantiated yet, ids will be used to reference accounts and libraries When a program is instantiated, the ids will be replaced by the instantiated addresses", + "oneOf": [ + { + "type": "object", + "required": [ + "|library_account_addr|" + ], + "properties": { + "|library_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|library_id|" + ], + "properties": { + "|library_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "LibraryConfigUpdate": { + "type": "object", + "properties": { + "denom": { + "type": [ + "string", + "null" + ] + }, + "input_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "output_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "pool_addr": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "Enum representing the different query messages that can be sent.", + "oneOf": [ + { + "description": "Query to get the processor address.", + "type": "object", + "required": [ + "get_processor" + ], + "properties": { + "get_processor": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query to get the library configuration.", + "type": "object", + "required": [ + "get_library_config" + ], + "properties": { + "get_library_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_raw_library_config" + ], + "properties": { + "get_raw_library_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "get_library_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "denom", + "input_addr", + "output_addr", + "pool_addr" + ], + "properties": { + "denom": { + "type": "string" + }, + "input_addr": { + "$ref": "#/definitions/Addr" + }, + "output_addr": { + "$ref": "#/definitions/Addr" + }, + "pool_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, + "get_processor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Addr", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "get_raw_library_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LibraryConfig", + "type": "object", + "required": [ + "denom", + "input_addr", + "output_addr", + "pool_addr" + ], + "properties": { + "denom": { + "type": "string" + }, + "input_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "output_addr": { + "description": "Address of the output account", + "allOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + } + ] + }, + "pool_addr": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "LibraryAccountType": { + "description": "A helper type that is used to associate an account or library with an id When a program is not instantiated yet, ids will be used to reference accounts and libraries When a program is instantiated, the ids will be replaced by the instantiated addresses", + "oneOf": [ + { + "type": "object", + "required": [ + "|library_account_addr|" + ], + "properties": { + "|library_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|library_id|" + ], + "properties": { + "|library_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + } + } + }, + "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" + } + } + } + } +} diff --git a/contracts/libraries/nolus-lending/src/bin/schema.rs b/contracts/libraries/nolus-lending/src/bin/schema.rs new file mode 100644 index 000000000..672e466d7 --- /dev/null +++ b/contracts/libraries/nolus-lending/src/bin/schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use valence_library_utils::msg::{ExecuteMsg, InstantiateMsg}; +use valence_nolus_lending::msg::{FunctionMsgs, LibraryConfig, LibraryConfigUpdate, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/libraries/nolus-lending/src/contract.rs b/contracts/libraries/nolus-lending/src/contract.rs new file mode 100644 index 000000000..b7cff67d5 --- /dev/null +++ b/contracts/libraries/nolus-lending/src/contract.rs @@ -0,0 +1,194 @@ +use crate::msg::{Config, FunctionMsgs, LibraryConfig, LibraryConfigUpdate, QueryMsg}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, to_json_string, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, + Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, +}; +use valence_library_utils::{ + error::LibraryError, + execute_on_behalf_of, execute_submsgs_on_behalf_of, + msg::{ExecuteMsg, InstantiateMsg}, +}; + +// version info for migration info +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Unique ID for reply handling +const WITHDRAW_REPLY_ID: u64 = 1; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + valence_library_base::instantiate(deps, CONTRACT_NAME, CONTRACT_VERSION, msg) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + valence_library_base::execute(deps, env, info, msg, process_function, update_config) +} + +pub fn process_function( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: FunctionMsgs, + cfg: Config, +) -> Result { + match msg { + FunctionMsgs::Lend {} => { + // Query account balance + let balance = deps + .querier + .query_balance(cfg.input_addr.clone(), cfg.denom.clone())?; + + if balance.amount.is_zero() { + return Err(LibraryError::ExecutionError("No funds to lend".to_string())); + } + + // Prepare lend message + let lend_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.pool_addr.to_string(), + msg: to_json_binary(&valence_lending_utils::nolus::ExecuteMsg::Deposit {})?, + funds: vec![balance.clone()], + }); + + // Execute on behalf of input_addr + let execute_msg = execute_on_behalf_of(vec![lend_msg], &cfg.input_addr)?; + + Ok(Response::new() + .add_message(execute_msg) + .add_attribute("method", "lend") + .add_attribute("amount", balance.to_string()) + .add_attribute("input_addr", cfg.input_addr.to_string())) + } + FunctionMsgs::Withdraw { amount } => { + // Check the nlp balance of the input address (lender) + let balance_nlpn_resp: valence_lending_utils::nolus::BalanceResponse = + deps.querier.query_wasm_smart( + cfg.pool_addr.to_string(), + &valence_lending_utils::nolus::QueryMsg::Balance { + address: cfg.input_addr.clone(), + }, + )?; + + let balance_nlp: Uint128 = balance_nlpn_resp.balance; + if balance_nlp.is_zero() { + return Err(LibraryError::ExecutionError( + "No funds to withdraw".to_string(), + )); + } + + // Check withdrawal amount + let withdrawal_amount = match amount { + // withdraw exact amount + Some(amt) => { + if amt > balance_nlp || amt.is_zero() { + return Err(LibraryError::ExecutionError( + "Withdraw amount is either zero or bigger than balance".to_string(), + )); + } + amt + } + // if no amount is specified, we withdraw the entire position + None => balance_nlp, + }; + + // Prepare withdraw message + let withdraw_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.pool_addr.to_string(), + msg: to_json_binary(&valence_lending_utils::nolus::ExecuteMsg::Burn { + amount: withdrawal_amount, + })?, + funds: vec![], + }); + + // Execute on behalf of input_addr with reply. On reply we will send the funds to the output address + let execute_msg = execute_submsgs_on_behalf_of( + vec![SubMsg::reply_on_success(withdraw_msg, WITHDRAW_REPLY_ID)], + Some(to_json_string(&cfg)?), + &cfg.input_addr, + )?; + + Ok(Response::new() + .add_submessage(SubMsg::reply_on_success(execute_msg, WITHDRAW_REPLY_ID)) + .add_attribute("method", "burn")) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + WITHDRAW_REPLY_ID => { + // Extract configuration from the reply payload + let cfg: Config = valence_account_utils::msg::parse_valence_payload(&msg.result)?; + + // Query account balance of input account after withdrawal + let balance = deps + .querier + .query_balance(cfg.input_addr.clone(), cfg.denom.clone())?; + + if balance.amount.is_zero() { + return Err(LibraryError::ExecutionError( + "No withdrawn funds".to_string(), + )); + } + + // Transfer the withdrawn funds to the output address + let send_msg = CosmosMsg::Bank(cosmwasm_std::BankMsg::Send { + to_address: cfg.output_addr.to_string(), + amount: vec![balance.clone()], + }); + + let execute_msg = execute_on_behalf_of(vec![send_msg], &cfg.input_addr)?; + + Ok(Response::new() + .add_message(execute_msg) + .add_attribute("method", "withdraw") + .add_attribute("amount", balance.to_string()) + .add_attribute("output_addr", cfg.output_addr.to_string())) + } + _ => Err(LibraryError::Std(StdError::generic_err("unknown reply id"))), + } +} + +pub fn update_config( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + new_config: LibraryConfigUpdate, +) -> Result<(), LibraryError> { + new_config.update_config(deps) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Ownership {} => { + to_json_binary(&valence_library_base::get_ownership(deps.storage)?) + } + QueryMsg::GetProcessor {} => { + to_json_binary(&valence_library_base::get_processor(deps.storage)?) + } + QueryMsg::GetLibraryConfig {} => { + let config: Config = valence_library_base::load_config(deps.storage)?; + to_json_binary(&config) + } + QueryMsg::GetRawLibraryConfig {} => { + let raw_config: LibraryConfig = + valence_library_utils::raw_config::query_raw_library_config(deps.storage)?; + to_json_binary(&raw_config) + } + } +} diff --git a/contracts/libraries/nolus-lending/src/lib.rs b/contracts/libraries/nolus-lending/src/lib.rs new file mode 100644 index 000000000..112ecadc8 --- /dev/null +++ b/contracts/libraries/nolus-lending/src/lib.rs @@ -0,0 +1,2 @@ +pub mod contract; +pub mod msg; diff --git a/contracts/libraries/nolus-lending/src/msg.rs b/contracts/libraries/nolus-lending/src/msg.rs new file mode 100644 index 000000000..314d4edb4 --- /dev/null +++ b/contracts/libraries/nolus-lending/src/msg.rs @@ -0,0 +1,130 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Deps, DepsMut, Uint128}; +use cw_ownable::cw_ownable_query; +use valence_library_utils::LibraryAccountType; +use valence_library_utils::{error::LibraryError, msg::LibraryConfigValidation}; +use valence_macros::{valence_library_query, ValenceLibraryInterface}; + +#[cw_serde] +pub enum FunctionMsgs { + /// Message to lend tokens. + Lend {}, + /// Message to withdraw tokens. If amount is not specified, full amount will be withdrawn. + Withdraw { amount: Option }, +} + +#[valence_library_query] +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +/// Enum representing the different query messages that can be sent. +pub enum QueryMsg {} + +#[cw_serde] +#[derive(ValenceLibraryInterface)] +pub struct LibraryConfig { + // Address of the input account (Valence interchain account) + pub input_addr: LibraryAccountType, + /// Address of the output account + pub output_addr: LibraryAccountType, + // Address of the pool contract + pub pool_addr: String, + // Denom of the asset we are going to lend + pub denom: String, +} + +impl LibraryConfig { + pub fn new( + input_addr: impl Into, + output_addr: impl Into, + pool_address: String, + denom: String, + ) -> Self { + LibraryConfig { + input_addr: input_addr.into(), + output_addr: output_addr.into(), + pool_addr: pool_address, + denom, + } + } + + fn do_validate(&self, api: &dyn cosmwasm_std::Api) -> Result<(Addr, Addr, Addr), LibraryError> { + let input_addr = self.input_addr.to_addr(api)?; + let output_addr = self.output_addr.to_addr(api)?; + let pool_addr = api.addr_validate(&self.pool_addr)?; + + Ok((input_addr, output_addr, pool_addr)) + } +} + +impl LibraryConfigValidation for LibraryConfig { + #[cfg(not(target_arch = "wasm32"))] + fn pre_validate(&self, api: &dyn cosmwasm_std::Api) -> Result<(), LibraryError> { + self.do_validate(api)?; + Ok(()) + } + + fn validate(&self, deps: Deps) -> Result { + let (input_addr, output_addr, pool_address) = self.do_validate(deps.api)?; + + Ok(Config { + input_addr, + output_addr, + pool_addr: pool_address, + denom: self.denom.clone(), + }) + } +} + +impl LibraryConfigUpdate { + pub fn update_config(self, deps: DepsMut) -> Result<(), LibraryError> { + let mut config: Config = valence_library_base::load_config(deps.storage)?; + + // First update input_addr (if needed) + if let Some(input_addr) = self.input_addr { + config.input_addr = input_addr.to_addr(deps.api)?; + } + + // Next update output_addr (if needed) + if let Some(output_addr) = self.output_addr { + config.output_addr = output_addr.to_addr(deps.api)?; + } + + // Next update pool_addr (if needed) + if let Some(pool_addr) = self.pool_addr { + config.pool_addr = deps.api.addr_validate(&pool_addr)?; + } + + // Next update denom (if needed) + if let Some(denom) = self.denom { + config.denom = denom; + } + + valence_library_base::save_config(deps.storage, &config)?; + Ok(()) + } +} + +#[cw_serde] +pub struct Config { + pub input_addr: Addr, + pub output_addr: Addr, + pub pool_addr: Addr, + pub denom: String, +} + +impl Config { + pub fn new( + input_address: Addr, + output_address: Addr, + pool_address: Addr, + denom: String, + ) -> Self { + Config { + input_addr: input_address, + output_addr: output_address, + pool_addr: pool_address, + denom, + } + } +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 5df37a205..b06f6820c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -39,6 +39,7 @@ - [Forwarder](./libraries/cosmwasm/forwarder.md) - [Generic IBC Transfer](./libraries/cosmwasm/generic_ibc_transfer.md) - [Neutron IBC Transfer](./libraries/cosmwasm/neutron_ibc_transfer.md) + - [Nolus Lending](./libraries/cosmwasm/nolus_lending.md) - [Osmosis CL LPer](./libraries/cosmwasm/osmosis_cl_lper.md) - [Osmosis CL Withdrawer](./libraries/cosmwasm/osmosis_cl_withdrawer.md) - [Osmosis GAMM LPer](./libraries/cosmwasm/osmosis_gamm_lper.md) diff --git a/docs/src/libraries/cosmwasm/nolus_lending.md b/docs/src/libraries/cosmwasm/nolus_lending.md new file mode 100644 index 000000000..becdcaa8f --- /dev/null +++ b/docs/src/libraries/cosmwasm/nolus_lending.md @@ -0,0 +1,68 @@ +# Valence Nolus Lending library + +The Valence Nolus Lending library facilitates lending operations on the [Nolus Protocol](https://nolus.io/) from an input account and manages withdrawal of lent assets to an output account. This library enables Valence Programs to earn yield on deposited assets through Nolus Protocol's lending markets while maintaining full control over the lending positions directly from the input account. + +## High Level Flow + +```mermaid +--- +title: Nolus Lending Library +--- +graph LR + IA((Input + Account)) + OA((Output + Account)) + P[Processor] + S[Nolus Lending + Library] + P -- 1/Lend or Withdraw --> S + S -- 2/Query balances --> IA + S -- 3/Execute Lending --> IA + S -- 5/Execute Withdrawal --> IA + IA -- 6/Transfer Assets --> OA +``` + +## Functions + +| Function | Parameters | Description | +|----------|------------|-------------| +| **Lend** | - | Lends the entire balance of the specified denom from the input account to the Nolus Protocol. | +| **Withdraw** | `amount: Option` | Withdraws lent assets from the input account to the output account. If no amount is specified, the entire position is withdrawn. The user specifies the amount of nLPN tokens, which are received upon lending the assets. | + +## Configuration + +The library is configured on instantiation via the `LibraryConfig` type. + +```rust +pub struct LibraryConfig { + // Address of the input account + pub input_addr: LibraryAccountType, + /// Address of the output account + pub output_addr: LibraryAccountType, + // Address of the pool contract + pub pool_addr: String, + // Denom of the asset we are going to lend + pub denom: String, +} +``` + +## Implementation Details + +### Lending Process + +1. **Balance Check**: Queries the input account balance for the specified denom. +2. **Lend**: There is a Liquidity Providers’ Pool (LPP) instance per denomination, serving all lenders that provide liquidity in that same currency. Upon deposits, lenders obtain an amount of CW20 interest-bearing token or nLPN. The entire amount of the specified denom will be lent to the Nolus Protocol. + +### Withdrawal Process + +1. **Balance Check**: Queries the balance of the nLPN. To withdraw LPN tokens, the wallet address needs to have a positive amount of nLPN tokens. +2. **Amount Calculation**: Uses the exact amount if specified; otherwise, withdraws the entire balance. If the user wishes to withdraw everything, the rewards will be claimed, and the user will no longer be a lender. +3. **Withdraw**: Executes a `Burn` message, which withdraws the lent position back to the input account. +4. **Reply Handling**: Uses the CosmWasm reply mechanism to handle the two-step process of withdrawal. Upon successful withdrawal, the funds will be transferred to the Valence output account. + +### Error Handling + +- **No Funds**: Returns an error if attempting to lend or withdraw with a zero balance. +- **Nolus Integration**: Propagates Nolus Protocol errors during lending or withdrawal operations. + diff --git a/packages/lending-utils/Cargo.toml b/packages/lending-utils/Cargo.toml index 333f71aa9..848c64be8 100644 --- a/packages/lending-utils/Cargo.toml +++ b/packages/lending-utils/Cargo.toml @@ -8,3 +8,4 @@ description = "Helpers for lending integrations" [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } +serde = { workspace = true } diff --git a/packages/lending-utils/src/lib.rs b/packages/lending-utils/src/lib.rs index 1957afaa5..b69288ad2 100644 --- a/packages/lending-utils/src/lib.rs +++ b/packages/lending-utils/src/lib.rs @@ -1 +1,2 @@ pub mod mars; +pub mod nolus; diff --git a/packages/lending-utils/src/nolus.rs b/packages/lending-utils/src/nolus.rs new file mode 100644 index 000000000..326a28ba5 --- /dev/null +++ b/packages/lending-utils/src/nolus.rs @@ -0,0 +1,37 @@ +// Since Nolus is using an older CosmWasm version, to make it compatible with our packages, we are going to redefine the messages here using Cosmwasm that we need +// for our library +// The content here is from https://github.com/nolus-protocol/nolus-money-market, which is the stable API for mars contracts + +use cosmwasm_std::{Addr, Uint128}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde( + deny_unknown_fields, + rename_all = "snake_case", + bound(serialize = "", deserialize = "") +)] +pub enum ExecuteMsg { + Deposit(), + // CW20 interface, withdraw from lender deposit + Burn { amount: Uint128 }, +} + +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde( + deny_unknown_fields, + rename_all = "snake_case", + bound(serialize = "", deserialize = "") +)] +pub enum QueryMsg { + // Deposit + /// CW20 interface, lender deposit balance + Balance { address: Addr }, +} + +// CW20 interface +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub struct BalanceResponse { + pub balance: Uint128, +} diff --git a/program-manager/Cargo.toml b/program-manager/Cargo.toml index 26974b32c..34a57c206 100644 --- a/program-manager/Cargo.toml +++ b/program-manager/Cargo.toml @@ -45,6 +45,7 @@ valence-neutron-ibc-transfer-library = { workspace = true } valence-drop-liquid-staker = { workspace = true } valence-drop-liquid-unstaker = { workspace = true } valence-mars-lending = { workspace = true } +valence-nolus-lending = { workspace = true } tokio = { workspace = true } aho-corasick = "1.1" diff --git a/program-manager/schema/valence-program-manager.json b/program-manager/schema/valence-program-manager.json index 885853ea2..870759b32 100644 --- a/program-manager/schema/valence-program-manager.json +++ b/program-manager/schema/valence-program-manager.json @@ -926,6 +926,18 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ValenceNolusLending" + ], + "properties": { + "ValenceNolusLending": { + "$ref": "#/definitions/LibraryConfig15" + } + }, + "additionalProperties": false } ] }, @@ -1054,6 +1066,35 @@ }, "additionalProperties": false }, + "LibraryConfig15": { + "type": "object", + "required": [ + "denom", + "input_addr", + "output_addr", + "pool_addr" + ], + "properties": { + "denom": { + "type": "string" + }, + "input_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "output_addr": { + "description": "Address of the output account", + "allOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + } + ] + }, + "pool_addr": { + "type": "string" + } + }, + "additionalProperties": false + }, "LibraryConfig2": { "description": "Struct representing the library configuration.", "type": "object", @@ -1450,6 +1491,18 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ValenceNolusLending" + ], + "properties": { + "ValenceNolusLending": { + "$ref": "#/definitions/LibraryConfigUpdate15" + } + }, + "additionalProperties": false } ] }, @@ -1651,6 +1704,44 @@ }, "additionalProperties": false }, + "LibraryConfigUpdate15": { + "type": "object", + "properties": { + "denom": { + "type": [ + "string", + "null" + ] + }, + "input_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "output_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "pool_addr": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "LibraryConfigUpdate2": { "type": "object", "properties": { diff --git a/program-manager/src/library.rs b/program-manager/src/library.rs index f86ad8cbe..f3ca28ea6 100644 --- a/program-manager/src/library.rs +++ b/program-manager/src/library.rs @@ -98,6 +98,7 @@ pub enum LibraryConfig { ValenceDropLiquidStaker(valence_drop_liquid_staker::msg::LibraryConfig), ValenceDropLiquidUnstaker(valence_drop_liquid_unstaker::msg::LibraryConfig), ValenceMarsLending(valence_mars_lending::msg::LibraryConfig), + ValenceNolusLending(valence_nolus_lending::msg::LibraryConfig), } impl LibraryConfig {