diff --git a/Cargo.lock b/Cargo.lock index 30ce6899..20758eef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,7 +37,7 @@ checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "astroport" version = "2.0.0" -source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#127d9f844e325029ba61fa776555f7bb7d9b85c3" +source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#cd7c02e8894b1953cc73c299c51010fe758a05f3" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "astroport" version = "2.0.0" -source = "git+https://github.com/neutron-org/neutron-tge-contracts.git#127d9f844e325029ba61fa776555f7bb7d9b85c3" +source = "git+https://github.com/neutron-org/neutron-tge-contracts.git#cd7c02e8894b1953cc73c299c51010fe758a05f3" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -164,7 +164,7 @@ dependencies = [ [[package]] name = "astroport-periphery" version = "1.1.0" -source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#127d9f844e325029ba61fa776555f7bb7d9b85c3" +source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#cd7c02e8894b1953cc73c299c51010fe758a05f3" dependencies = [ "astroport 3.11.0", "cosmwasm-schema", @@ -2089,6 +2089,60 @@ dependencies = [ "thiserror", ] +[[package]] +name = "neutron-flashloans" +version = "0.3.0" +dependencies = [ + "anyhow", + "cosmos-sdk-proto", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-multi-test", + "cw-paginate 0.2.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cwd-interface", + "cwd-macros", + "neutron-sdk 0.8.0", + "prost 0.12.4", + "prost-types", + "schemars", + "serde", + "serde-json-wasm 1.0.1", + "serde_with", + "thiserror", +] + +[[package]] +name = "neutron-flashloans-user" +version = "0.3.0" +dependencies = [ + "anyhow", + "cosmos-sdk-proto", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-multi-test", + "cw-paginate 0.2.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cwd-interface", + "cwd-macros", + "neutron-sdk 0.8.0", + "prost 0.12.4", + "prost-types", + "schemars", + "serde", + "serde-json-wasm 1.0.1", + "serde_with", + "thiserror", +] + [[package]] name = "neutron-lockdrop-vault" version = "0.1.0" @@ -3036,7 +3090,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-base" version = "1.1.0" -source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#127d9f844e325029ba61fa776555f7bb7d9b85c3" +source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#cd7c02e8894b1953cc73c299c51010fe758a05f3" dependencies = [ "astroport 2.0.0 (git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main)", "cosmwasm-schema", @@ -3066,7 +3120,7 @@ dependencies = [ [[package]] name = "vesting-base" version = "1.1.0" -source = "git+https://github.com/neutron-org/neutron-tge-contracts.git#127d9f844e325029ba61fa776555f7bb7d9b85c3" +source = "git+https://github.com/neutron-org/neutron-tge-contracts.git#cd7c02e8894b1953cc73c299c51010fe758a05f3" dependencies = [ "astroport 2.0.0 (git+https://github.com/neutron-org/neutron-tge-contracts.git)", "cosmwasm-schema", @@ -3093,7 +3147,7 @@ dependencies = [ [[package]] name = "vesting-lp" version = "1.2.0" -source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#127d9f844e325029ba61fa776555f7bb7d9b85c3" +source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#cd7c02e8894b1953cc73c299c51010fe758a05f3" dependencies = [ "astroport 2.0.0 (git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main)", "cosmwasm-schema", @@ -3108,7 +3162,7 @@ dependencies = [ [[package]] name = "vesting-lp-pcl" version = "1.1.0" -source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#127d9f844e325029ba61fa776555f7bb7d9b85c3" +source = "git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main#cd7c02e8894b1953cc73c299c51010fe758a05f3" dependencies = [ "astroport 2.0.0 (git+https://github.com/neutron-org/neutron-tge-contracts.git?branch=main)", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index c4e12f08..1449f2bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = [ "contracts/dao/cwd-core", "contracts/dao/neutron-chain-manager", + "contracts/dao/neutron-flashloans", + "contracts/dao/neutron-flashloans-user", "contracts/dao/proposal/*", "contracts/dao/pre-propose/*", "contracts/dao/voting/*", diff --git a/contracts/dao/neutron-flashloans-user/.cargo/config b/contracts/dao/neutron-flashloans-user/.cargo/config new file mode 100644 index 00000000..5cea26c1 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example neutron-flashloans-user_schema" diff --git a/contracts/dao/neutron-flashloans-user/.gitignore b/contracts/dao/neutron-flashloans-user/.gitignore new file mode 100644 index 00000000..dfdaaa6b --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/dao/neutron-flashloans-user/Cargo.toml b/contracts/dao/neutron-flashloans-user/Cargo.toml new file mode 100644 index 00000000..82a5cdd4 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/Cargo.toml @@ -0,0 +1,41 @@ +[package] +authors = ["Andrei Zavgorodnii "] +description = "An implementation of a Flashloans user for Neutron" +edition = "2021" +name = "neutron-flashloans-user" +repository = "https://github.com/neutron-org/neutron-dao" +version = "0.3.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = {version = "1.3.0"} +cosmwasm-std = {version = "1.3.0"} +cosmwasm-storage = {version = "1.3.0"} +cw-controllers = "1.1.0" +cw-paginate = {path = "../../../packages/cw-paginate"} +cw-storage-plus = "1.1.0" +cw-utils = {version = "1.0.1"} +cw2 = "1.1.0" +cwd-interface = {path = "../../../packages/cwd-interface"} +cwd-macros = {path = "../../../packages/cwd-macros"} +schemars = "0.8.8" +serde = {version = "1.0.175", default-features = false, features = ["derive"]} +serde_with = {version = "3.7.0", features = ["json"]} +thiserror = {version = "1.0"} +neutron-sdk = "0.8.0" +serde-json-wasm = "1.0.1" +prost = { version = "0.12.3", default-features = false } +prost-types = { version = "0.12.3", default-features = false } +cosmos-sdk-proto = { version = "0.20.0", default-features = false } + +[dev-dependencies] +anyhow = "1.0.57" +cw-multi-test = "0.16.5" diff --git a/contracts/dao/neutron-flashloans-user/README.md b/contracts/dao/neutron-flashloans-user/README.md new file mode 100644 index 00000000..279cc6ec --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/README.md @@ -0,0 +1,6 @@ +### Neutron Flashloans User + +This is an internal contract used to test the functionality of the Neutron Flashloans contract. + +**This is not an example of a well-written borrower contract (since it does not implement any security checks) and +should not be used as a reference.** \ No newline at end of file diff --git a/contracts/dao/neutron-flashloans-user/examples/neutron-flashloans-user_schema.rs b/contracts/dao/neutron-flashloans-user/examples/neutron-flashloans-user_schema.rs new file mode 100644 index 00000000..b8afd51f --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/examples/neutron-flashloans-user_schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use neutron_flashloans_user::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/dao/neutron-flashloans-user/schema/neutron-flashloans-user.json b/contracts/dao/neutron-flashloans-user/schema/neutron-flashloans-user.json new file mode 100644 index 00000000..5f7b7014 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/schema/neutron-flashloans-user.json @@ -0,0 +1,139 @@ +{ + "contract_name": "neutron-flashloans-user", + "contract_version": "0.3.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "request_loan" + ], + "properties": { + "request_loan": { + "type": "object", + "required": [ + "amount", + "execution_mode", + "flashloans_contract" + ], + "properties": { + "amount": { + "description": "The amount to request.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "execution_mode": { + "description": "Determines how the loan should be processed.\n\nMODE_RETURN_LOAN = 0 (return the correct amount) MODE_WITHHOLD_LOAN = 1 (not return anything) MODE_RETURN_LOAN_MORE_THAN_NECESSARY = 2 (return more than expected) MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY = 3 (request another loan while processing the existing loan)\n\nAny other value will result in returning an error.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "flashloans_contract": { + "description": "Address to get the flashloans from.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "process_loan" + ], + "properties": { + "process_loan": { + "type": "object", + "required": [ + "fee", + "loan_amount", + "return_address" + ], + "properties": { + "fee": { + "description": "Specifies the fee which the borrower must pay to the return_address.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "loan_amount": { + "description": "Specifies the loan amount which the borrower must return to the return_address.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "return_address": { + "description": "Specifies the address to which the borrower must return the loan amount AND pay the fees.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "type": "string", + "enum": [] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": {} +} diff --git a/contracts/dao/neutron-flashloans-user/schema/raw/execute.json b/contracts/dao/neutron-flashloans-user/schema/raw/execute.json new file mode 100644 index 00000000..48d0151c --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/schema/raw/execute.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "request_loan" + ], + "properties": { + "request_loan": { + "type": "object", + "required": [ + "amount", + "execution_mode", + "flashloans_contract" + ], + "properties": { + "amount": { + "description": "The amount to request.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "execution_mode": { + "description": "Determines how the loan should be processed.\n\nMODE_RETURN_LOAN = 0 (return the correct amount) MODE_WITHHOLD_LOAN = 1 (not return anything) MODE_RETURN_LOAN_MORE_THAN_NECESSARY = 2 (return more than expected) MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY = 3 (request another loan while processing the existing loan)\n\nAny other value will result in returning an error.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "flashloans_contract": { + "description": "Address to get the flashloans from.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "process_loan" + ], + "properties": { + "process_loan": { + "type": "object", + "required": [ + "fee", + "loan_amount", + "return_address" + ], + "properties": { + "fee": { + "description": "Specifies the fee which the borrower must pay to the return_address.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "loan_amount": { + "description": "Specifies the loan amount which the borrower must return to the return_address.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "return_address": { + "description": "Specifies the address to which the borrower must return the loan amount AND pay the fees.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$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" + } + } +} diff --git a/contracts/dao/neutron-flashloans-user/schema/raw/instantiate.json b/contracts/dao/neutron-flashloans-user/schema/raw/instantiate.json new file mode 100644 index 00000000..1352613d --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/schema/raw/instantiate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/dao/neutron-flashloans-user/schema/raw/migrate.json b/contracts/dao/neutron-flashloans-user/schema/raw/migrate.json new file mode 100644 index 00000000..7fbe8c57 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/dao/neutron-flashloans-user/schema/raw/query.json b/contracts/dao/neutron-flashloans-user/schema/raw/query.json new file mode 100644 index 00000000..0f592a1a --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/schema/raw/query.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "type": "string", + "enum": [] +} diff --git a/contracts/dao/neutron-flashloans-user/schema/raw/response_to_config.json b/contracts/dao/neutron-flashloans-user/schema/raw/response_to_config.json new file mode 100644 index 00000000..0ea02c38 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/schema/raw/response_to_config.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "fee_rate", + "owner", + "source" + ], + "properties": { + "fee_rate": { + "description": "Defines the fee rate for the loans, e.g., fee_rate = 0.01 means that if you borrow 100untrn, you'll need to return 101untrn.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "owner": { + "description": "Defines the address that can add and remove sources.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "source": { + "description": "Defines the address that is going to sponsor the loans. This address needs to grant a (Generic)Authorization to this contract to execute /cosmos.bank.v1beta1.MsgSend on its behalf.", + "allOf": [ + { + "$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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } +} diff --git a/contracts/dao/neutron-flashloans-user/src/contract.rs b/contracts/dao/neutron-flashloans-user/src/contract.rs new file mode 100644 index 00000000..9a87f12d --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/src/contract.rs @@ -0,0 +1,166 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Response, StdError, StdResult, Uint128, WasmMsg, +}; +use cw2::set_contract_version; +use neutron_sdk::bindings::msg::NeutronMsg; + +use crate::error::ContractError; +use crate::msg::FlashloansExecuteMsg::RequestLoan; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{EXECUTION_MODE, FLASHLOANS_CONTRACT}; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:neutron-flashloans-user"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const MODE_RETURN_LOAN: u64 = 0; +pub const MODE_WITHHOLD_LOAN: u64 = 1; +pub const MODE_RETURN_LOAN_MORE_THAN_NECESSARY: u64 = 2; +pub const MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY: u64 = 3; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + EXECUTION_MODE.save(deps.storage, &0u64)?; + Ok(Response::new().add_attribute("action", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + match msg { + ExecuteMsg::RequestLoan { + flashloans_contract, + execution_mode, + amount, + } => execute_request_loan(deps, flashloans_contract, execution_mode, amount), + ExecuteMsg::ProcessLoan { + return_address, + loan_amount, + fee, + } => execute_process_loan(deps, return_address, loan_amount, fee), + } +} + +/// Makes the neutron-flashloans-user contract request a loan. Allows to +/// specify the execution mode (how the contract must behave while handling +/// the ProcessLoan message, see msg.rs), which is required for the integration +/// tests. Also allows to specify the loan amount. +pub fn execute_request_loan( + deps: DepsMut, + flashloans_contract: Addr, + execution_mode: u64, + amount: Vec, +) -> Result, ContractError> { + EXECUTION_MODE.save(deps.storage, &execution_mode)?; + // Saving the flashloans contract address is necessary for the + // MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY execution mode. + FLASHLOANS_CONTRACT.save(deps.storage, &flashloans_contract)?; + + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: flashloans_contract.to_string(), + msg: to_json_binary(&RequestLoan { amount }).unwrap(), + funds: vec![], + }); + Ok(Response::new() + .add_message(msg) + .add_attribute("action", "execute_request_loan")) +} + +pub fn execute_process_loan( + deps: DepsMut, + return_address: Addr, + loan_amount: Vec, + fee: Vec, +) -> Result, ContractError> { + let execution_mode = EXECUTION_MODE.load(deps.storage)?; + + match execution_mode { + // Return the correct amount + MODE_RETURN_LOAN => { + let mut return_amount: Vec = vec![]; + for (idx, coin) in loan_amount.iter().enumerate() { + return_amount.push(Coin::new( + (coin.amount + fee[idx].amount).u128(), + coin.denom.clone(), + )) + } + + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: return_address.to_string(), + amount: return_amount, + }); + + Ok(Response::new() + .add_message(msg) + .add_attribute("action", "execute_process_loan_MODE_RETURN_LOAN")) + } + // Do not return the loan + MODE_WITHHOLD_LOAN => { + Ok(Response::new().add_attribute("action", "execute_process_loan_MODE_WITHHOLD_LOAN")) + } + // Return more that necessary + MODE_RETURN_LOAN_MORE_THAN_NECESSARY => { + let mut return_amount: Vec = vec![]; + for (idx, coin) in loan_amount.iter().enumerate() { + return_amount.push(Coin::new( + // Simply add 1 + (coin.amount + fee[idx].amount + Uint128::one()).u128(), + coin.denom.clone(), + )) + } + + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: return_address.to_string(), + amount: return_amount, + }); + + Ok(Response::new().add_message(msg).add_attribute( + "action", + "execute_process_loan_MODE_RETURN_LOAN_MORE_THAN_NECESSARY", + )) + } + // Request another loan while processing the existing loan + MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY => { + let flashloans_contract = FLASHLOANS_CONTRACT.load(deps.storage)?; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: flashloans_contract.to_string(), + msg: to_json_binary(&RequestLoan { + amount: vec![Coin::new(100u128, "untrn")], + }) + .unwrap(), + funds: vec![], + }); + Ok(Response::new().add_message(msg).add_attribute( + "action", + "execute_request_loan_MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY", + )) + } + _ => Err(ContractError::Std(StdError::generic_err( + "The ProcessLoan handler failed", + ))), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { + Ok(Binary::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} diff --git a/contracts/dao/neutron-flashloans-user/src/error.rs b/contracts/dao/neutron-flashloans-user/src/error.rs new file mode 100644 index 00000000..7155f592 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/src/error.rs @@ -0,0 +1,8 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), +} diff --git a/contracts/dao/neutron-flashloans-user/src/lib.rs b/contracts/dao/neutron-flashloans-user/src/lib.rs new file mode 100644 index 00000000..d78159fb --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; +mod error; +pub mod msg; +pub mod state; diff --git a/contracts/dao/neutron-flashloans-user/src/msg.rs b/contracts/dao/neutron-flashloans-user/src/msg.rs new file mode 100644 index 00000000..60193c26 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/src/msg.rs @@ -0,0 +1,50 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin}; + +#[cw_serde] +#[serde(rename_all = "snake_case")] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + RequestLoan { + /// Address to get the flashloans from. + flashloans_contract: Addr, + + /// Determines how the loan should be processed. + /// + /// MODE_RETURN_LOAN = 0 (return the correct amount) + /// MODE_WITHHOLD_LOAN = 1 (not return anything) + /// MODE_RETURN_LOAN_MORE_THAN_NECESSARY = 2 (return more than expected) + /// MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY = 3 (request another loan while processing the existing loan) + /// + /// Any other value will result in returning an error. + execution_mode: u64, + + /// The amount to request. + amount: Vec, + }, + ProcessLoan { + /// Specifies the address to which the borrower must return the loan amount AND pay the fees. + return_address: Addr, + /// Specifies the loan amount which the borrower must return to the return_address. + loan_amount: Vec, + /// Specifies the fee which the borrower must pay to the return_address. + fee: Vec, + }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg {} + +#[cw_serde] +pub enum FlashloansExecuteMsg { + /// This message duplicates the neutron-flashloan RequestLoan message + /// to avoid moving it to packages. + RequestLoan { amount: Vec }, +} + +#[cw_serde] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} diff --git a/contracts/dao/neutron-flashloans-user/src/state.rs b/contracts/dao/neutron-flashloans-user/src/state.rs new file mode 100644 index 00000000..d43ae431 --- /dev/null +++ b/contracts/dao/neutron-flashloans-user/src/state.rs @@ -0,0 +1,10 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +/// Stores the execution mode of the neutron-flashloans-user contract. See msg.rs for more details. +pub const EXECUTION_MODE: Item = Item::new("neutron-flashloans-user-execution-mode"); + +/// Stores the address of the flashloans contract. It is necessary for the +/// MODE_REQUEST_ANOTHER_LOAN_RECURSIVELY execution mode. +pub const FLASHLOANS_CONTRACT: Item = + Item::new("neutron-flashloans-user-flashloans-contract"); diff --git a/contracts/dao/neutron-flashloans/.cargo/config b/contracts/dao/neutron-flashloans/.cargo/config new file mode 100644 index 00000000..1fbeb10f --- /dev/null +++ b/contracts/dao/neutron-flashloans/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example neutron-flashloans_schema" diff --git a/contracts/dao/neutron-flashloans/.gitignore b/contracts/dao/neutron-flashloans/.gitignore new file mode 100644 index 00000000..dfdaaa6b --- /dev/null +++ b/contracts/dao/neutron-flashloans/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/dao/neutron-flashloans/Cargo.toml b/contracts/dao/neutron-flashloans/Cargo.toml new file mode 100644 index 00000000..33834768 --- /dev/null +++ b/contracts/dao/neutron-flashloans/Cargo.toml @@ -0,0 +1,41 @@ +[package] +authors = ["Andrei Zavgorodnii "] +description = "An implementation of Flashloans for Neutron" +edition = "2021" +name = "neutron-flashloans" +repository = "https://github.com/neutron-org/neutron-dao" +version = "0.3.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = {version = "1.3.0"} +cosmwasm-std = {version = "1.3.0"} +cosmwasm-storage = {version = "1.3.0"} +cw-controllers = "1.1.0" +cw-paginate = {path = "../../../packages/cw-paginate"} +cw-storage-plus = "1.1.0" +cw-utils = {version = "1.0.1"} +cw2 = "1.1.0" +cwd-interface = {path = "../../../packages/cwd-interface"} +cwd-macros = {path = "../../../packages/cwd-macros"} +schemars = "0.8.8" +serde = {version = "1.0.175", default-features = false, features = ["derive"]} +serde_with = {version = "3.7.0", features = ["json"]} +thiserror = {version = "1.0"} +neutron-sdk = "0.8.0" +serde-json-wasm = "1.0.1" +prost = { version = "0.12.3", default-features = false } +prost-types = { version = "0.12.3", default-features = false } +cosmos-sdk-proto = { version = "0.20.0", default-features = false } + +[dev-dependencies] +anyhow = "1.0.57" +cw-multi-test = "0.16.5" diff --git a/contracts/dao/neutron-flashloans/README.md b/contracts/dao/neutron-flashloans/README.md new file mode 100644 index 00000000..6363a546 --- /dev/null +++ b/contracts/dao/neutron-flashloans/README.md @@ -0,0 +1,66 @@ +# Neutron Flashloans + +## Overview + +The `neutron-flashloans` contract facilitates providing flash loans to smart contracts operating on the Neutron network. + +A flash loan is a type of uncollateralized loan in the cryptocurrency and decentralized finance (DeFi) space. It allows +borrowers to borrow funds without providing any collateral, on the condition that the loan is repaid within the same +transaction. If the borrower fails to repay the loan by the end of the transaction, the entire transaction is +reversed, effectively canceling the loan. Flash loans are typically used for arbitrage, collateral swapping, and +refinancing, taking advantage of price discrepancies or temporary liquidity needs without requiring long-term capital. + +## Usage + +To get a flash loan, a `RequestLoan` message needs to be sent to the `neutron-flashloans` contract: + +```rust +struct RequestLoan { + /// The amount that the borrower contract requests; there should be no + /// duplicate denoms and no zero amounts. + amount: Vec, +} +``` + +The sender needs to be a smart-contract that implements a handler for the `ProcessLoan` message: + +```rust +#[cw_serde] +pub enum BorrowerInterface { + ProcessLoan { + /// Specifies the address to which the borrower must return the loan amount AND pay the fees. + return_address: Addr, + /// Specifies the loan amount which the borrower must return to the return_address. + loan_amount: Vec, + /// Specifies the fee which the borrower must pay to the return_address. + fee: Vec, + } +} +``` + +Upon receiving the `RequestLoan` message, the `neutron-flashloans` contract will transfer the requested amount to the +borrower and send a `ProcessLoan` message. The borrower can execute any logic within its `ProcessLoan` handler but must +return the `loan_amount` plus the `fee` to the `return_address`. Failure to do so will result in the entire transaction +being reverted. + +## Implementation + +The `neutron-flashloans` contract does not hold any funds. Instead, it uses `authz` permission from the `source` address +to execute `/cosmos.bank.v1beta1.MsgSend` on its behalf. For Neutron, the `source` address must be set to the Treasury ( +DAO core) contract address. + +* The `RequestLoan` handler ensures there is no active loan, validates the loan amount (no duplicate or zero coins), + calculates the expected balance (current balance + fee) of the source after repayment, and records the loan details in + storage. If the `source` does not have the requested amount of funds, an error will be returned. Finally, it instructs + the source to send the requested amount to the borrower via `authz`, encapsulated in a `stargate message`. This + message is submitted as a submessage with a `reply_on_success` strategy, meaning if it fails, the transaction is + reverted. +* Upon successful execution of the `/cosmos.bank.v1beta1.MsgSend` message, the `neutron-flashloans` contract sends + a `ProcessLoan` submessage with a `reply_on_success` strategy to the borrower contract. +* After receiving a successful reply to the `ProcessLoan` message, the `neutron-flashloans` contract verifies that the + borrower has returned the funds and paid the fee, then it deletes the loan information. + +## Security advice + +When writing a borrower contract, ensure that the `ProcessLoan` handler has proper permissions. It should only be +callable when your contract has previously requested a loan and only by the `neutron-flashloans` contract. diff --git a/contracts/dao/neutron-flashloans/examples/neutron-flashloans_schema.rs b/contracts/dao/neutron-flashloans/examples/neutron-flashloans_schema.rs new file mode 100644 index 00000000..22004255 --- /dev/null +++ b/contracts/dao/neutron-flashloans/examples/neutron-flashloans_schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use neutron_flashloans::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/dao/neutron-flashloans/schema/neutron-flashloans.json b/contracts/dao/neutron-flashloans/schema/neutron-flashloans.json new file mode 100644 index 00000000..6e895c55 --- /dev/null +++ b/contracts/dao/neutron-flashloans/schema/neutron-flashloans.json @@ -0,0 +1,233 @@ +{ + "contract_name": "neutron-flashloans", + "contract_version": "0.3.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "fee_rate", + "owner", + "source" + ], + "properties": { + "fee_rate": { + "description": "Defines the fee rate for the loans, e.g., fee_rate = 0.01 means that if you borrow 100untrn, you'll need to return 101untrn.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "owner": { + "description": "Defines the owner of the contract. The owner can add and remove sources from the contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "source": { + "description": "Defines the address that is going to sponsor the loans. This address needs to grant a (Generic)Authorization to this contract to execute /cosmos.bank.v1beta1.MsgSend on its behalf.", + "allOf": [ + { + "$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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "request_loan" + ], + "properties": { + "request_loan": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount that the borrower contract requests; there should be no duplicate denoms and no zero amounts.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "fee_rate": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "source": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Returns the current config value.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "fee_rate", + "owner", + "source" + ], + "properties": { + "fee_rate": { + "description": "Defines the fee rate for the loans, e.g., fee_rate = 0.01 means that if you borrow 100untrn, you'll need to return 101untrn.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "owner": { + "description": "Defines the address that can modify the configuration of the contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "source": { + "description": "Defines the address that is going to sponsor the loans. This address needs to grant a (Generic)Authorization to this contract to execute /cosmos.bank.v1beta1.MsgSend on its behalf.", + "allOf": [ + { + "$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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } + } + } +} diff --git a/contracts/dao/neutron-flashloans/schema/raw/execute.json b/contracts/dao/neutron-flashloans/schema/raw/execute.json new file mode 100644 index 00000000..a1fa236c --- /dev/null +++ b/contracts/dao/neutron-flashloans/schema/raw/execute.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "request_loan" + ], + "properties": { + "request_loan": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount that the borrower contract requests; there should be no duplicate denoms and no zero amounts.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "fee_rate": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "source": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/dao/neutron-flashloans/schema/raw/instantiate.json b/contracts/dao/neutron-flashloans/schema/raw/instantiate.json new file mode 100644 index 00000000..4e0c3b00 --- /dev/null +++ b/contracts/dao/neutron-flashloans/schema/raw/instantiate.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "fee_rate", + "owner", + "source" + ], + "properties": { + "fee_rate": { + "description": "Defines the fee rate for the loans, e.g., fee_rate = 0.01 means that if you borrow 100untrn, you'll need to return 101untrn.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "owner": { + "description": "Defines the owner of the contract. The owner can add and remove sources from the contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "source": { + "description": "Defines the address that is going to sponsor the loans. This address needs to grant a (Generic)Authorization to this contract to execute /cosmos.bank.v1beta1.MsgSend on its behalf.", + "allOf": [ + { + "$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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } +} diff --git a/contracts/dao/neutron-flashloans/schema/raw/migrate.json b/contracts/dao/neutron-flashloans/schema/raw/migrate.json new file mode 100644 index 00000000..7fbe8c57 --- /dev/null +++ b/contracts/dao/neutron-flashloans/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/dao/neutron-flashloans/schema/raw/query.json b/contracts/dao/neutron-flashloans/schema/raw/query.json new file mode 100644 index 00000000..0c289ff3 --- /dev/null +++ b/contracts/dao/neutron-flashloans/schema/raw/query.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Returns the current config value.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/dao/neutron-flashloans/schema/raw/response_to_config.json b/contracts/dao/neutron-flashloans/schema/raw/response_to_config.json new file mode 100644 index 00000000..0c3c4610 --- /dev/null +++ b/contracts/dao/neutron-flashloans/schema/raw/response_to_config.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "fee_rate", + "owner", + "source" + ], + "properties": { + "fee_rate": { + "description": "Defines the fee rate for the loans, e.g., fee_rate = 0.01 means that if you borrow 100untrn, you'll need to return 101untrn.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "owner": { + "description": "Defines the address that can modify the configuration of the contract.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "source": { + "description": "Defines the address that is going to sponsor the loans. This address needs to grant a (Generic)Authorization to this contract to execute /cosmos.bank.v1beta1.MsgSend on its behalf.", + "allOf": [ + { + "$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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } +} diff --git a/contracts/dao/neutron-flashloans/src/contract.rs b/contracts/dao/neutron-flashloans/src/contract.rs new file mode 100644 index 00000000..ef623ed0 --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/contract.rs @@ -0,0 +1,403 @@ +use cosmos_sdk_proto::cosmos::{ + authz::v1beta1::MsgExec, bank::v1beta1::MsgSend, base::v1beta1::Coin as ProtoCoin, +}; +use cosmos_sdk_proto::traits::Message; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, AllBalanceResponse, BankQuery, Binary, Coin, CosmosMsg, Decimal, Deps, + DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, Uint128, WasmMsg, +}; +use cw2::set_contract_version; +use neutron_sdk::bindings::msg::NeutronMsg; +use prost_types::Any; +use std::collections::HashSet; + +use crate::error::ContractError; +use crate::error::ContractError::FlashloanAlreadyActive; +use crate::msg::{BorrowerInterface, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{ActiveLoan, Config, ACTIVE_LOAN, CONFIG}; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:neutron-flashloans"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Used to identify a reply to the /cosmos.bank.v1beta1.MsgSend message +/// that we execute immediately after receiving a loan request in the +/// RequestLoan handler. +pub const AUTHZ_BANK_SEND_REPLY_ID: u64 = 0; + +/// Used to identify a reply to the ProcessLoan message that we send to +/// the borrower after transferring them the loan. +pub const BORROWER_HANDLER_REPLY_ID: u64 = 1; + +pub const BANK_MSG_SEND_TYPE_URL: &str = "/cosmos.bank.v1beta1.MsgSend"; +pub const AUTHZ_MSG_EXEC_TYPE_URL: &str = "/cosmos.authz.v1beta1.MsgExec"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Signifies that we start with no active loan + ACTIVE_LOAN.save(deps.storage, &None)?; + + CONFIG.save( + deps.storage, + &Config { + owner: msg.owner.clone(), + fee_rate: msg.fee_rate, + source: msg.source.clone(), + }, + )?; + + Ok(Response::new() + .add_attribute("owner", msg.owner.to_string()) + .add_attribute("fee_rate", msg.fee_rate.to_string()) + .add_attribute("source", msg.source.to_string()) + .add_attribute("action", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + match msg { + ExecuteMsg::UpdateConfig { + owner, + source, + fee_rate, + } => execute_update_config(deps, info, owner, source, fee_rate), + ExecuteMsg::RequestLoan { amount } => execute_request_loan(deps, env, info, amount), + } +} + +/// Updates the config with values provided by the owner. +pub fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + owner: Option, + source: Option, + fee_rate: Option, +) -> Result, ContractError> { + let mut config: Config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + if let Some(new_owner) = owner { + config.owner = new_owner; + } + if let Some(new_source) = source { + config.source = new_source; + } + // No fee rate validation is required here because we can properly process + // any valid Decimal number + if let Some(new_fee_rate) = fee_rate { + config.fee_rate = new_fee_rate; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "execute_update_config") + .add_attribute("owner", config.owner.to_string()) + .add_attribute("source", config.source.to_string()) + .add_attribute("fee_rate", config.fee_rate.to_string())) +} + +/// This handler ensures there is no active loan, validates the loan amount (no duplicate or zero +/// coins), calculates the expected balance (current balance + fee) of the source after repayment, +/// records the loan details in storage. If the `source` does not have the requested amount of +/// funds, an error will be returned. Finally, it instructs the source to send the requested amount +/// to the borrower via `authz`, encapsulated in a `stargate message`. This message is submitted as +/// a submessage with a `reply_on_success` strategy, meaning if it fails, the transaction is +/// reverted. +pub fn execute_request_loan( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Vec, +) -> Result, ContractError> { + let config: Config = CONFIG.load(deps.storage)?; + + // Reentrancy guard + if ACTIVE_LOAN.load(deps.storage)?.is_some() { + return Err(FlashloanAlreadyActive {}); + } + + // Check that the amount does not have duplicate or zero coins + validate_amount(amount.clone())?; + + // Getting the current balances of the source is necessary to make sure that the loan + // was returned, and the fee was paid. + let pre_loan_balances = get_pre_loan_balances(&deps, config.source.clone(), amount.clone())?; + + // Calculate the fee to be paid by the borrower for receiving the loan + let fee = calculate_fee(amount.clone(), config.fee_rate)?; + + // Calculate the expected balance of the source after the loan has been returned + // and the fee has been paid as (requested amount + fee). + let expected_balances = calculate_expected_balances(pre_loan_balances, fee.clone())?; + + // Save all the information necessary to continue processing the loan request in the reply() + // handler. + ACTIVE_LOAN.save( + deps.storage, + &Some(ActiveLoan { + borrower: info.sender.clone(), + amount: amount.clone(), + fee, + expected_balances, + }), + )?; + + // Send a (stargate -> authz -> bank) /cosmos.bank.v1beta1.MsgSend submessage with + // reply_on_success strategy (we want the transaction to be simply reverted in case of an + // error). + let msg_send = get_stargate_authz_bank_send_msg(env, config, info.sender, amount); + Ok(Response::new() + .add_submessage(SubMsg::reply_on_success(msg_send, AUTHZ_BANK_SEND_REPLY_ID)) + .add_attribute("action", "execute_get_loan")) +} + +// Check that the amount does not have duplicate or zero coins +fn validate_amount(amount: Vec) -> Result<(), ContractError> { + let mut denoms: HashSet = HashSet::new(); + for coin in amount { + if coin.amount.eq(&Uint128::zero()) { + return Err(ContractError::ZeroRequested { denom: coin.denom }); + } + + if denoms.contains(&coin.denom) { + return Err(ContractError::DuplicateDenoms {}); + } + + denoms.insert(coin.denom); + } + + Ok(()) +} + +/// Returns the list of current balances on the contract's account for the coins that were +/// requested by the borrower. If any actual coin balance is lower than the requested amount, +/// returns an error. +fn get_pre_loan_balances( + deps: &DepsMut, + source: Addr, + requested_amount: Vec, +) -> Result, ContractError> { + // Prepare the query + let all_balances_query = BankQuery::AllBalances { + address: source.to_string(), + }; + // Get the response (all balances) + let all_balances_response: AllBalanceResponse = + deps.querier.query(&all_balances_query.into())?; + + // Filter all balances leaving only the balances of the requested coins + let mut pre_loan_balances: Vec = vec![]; + for requested_coin in requested_amount { + // Look for the requested coin in the source balances, AND check that the source + // has enough of the requested coin. + let maybe_source_coin = all_balances_response + .amount + .iter() + .find(|x| x.denom == requested_coin.denom && requested_coin.amount.le(&x.amount)); + + // If the source doesn't have (enough of) the requested coin, return an error + if maybe_source_coin.is_none() { + return Err(ContractError::InsufficientFunds { + denom: requested_coin.denom, + }); + } + + let source_coin = maybe_source_coin.unwrap(); + pre_loan_balances.push(source_coin.clone()) + } + + Ok(pre_loan_balances) +} + +/// Calculates the fee by multiplying each of the requested assets by fee_rate. +fn calculate_fee( + requested_amount: Vec, + fee_rate: Decimal, +) -> Result, ContractError> { + let mut fee: Vec = vec![]; + for coin in requested_amount { + let coin_fee = Coin::new((fee_rate * coin.amount).u128(), coin.denom); + fee.push(coin_fee) + } + + Ok(fee) +} + +// Sums the pre_loan_balances with the fee. +// WARNING: this function assumes that the input vectors are of the same length, +// and that the order of the denoms is the same. +fn calculate_expected_balances( + pre_loan_balances: Vec, + fee: Vec, +) -> Result, ContractError> { + let mut expected_balances: Vec = vec![]; + for (index, coin) in pre_loan_balances.iter().enumerate() { + expected_balances.push(Coin::new( + coin.amount.checked_add(fee[index].amount)?.u128(), + coin.denom.clone(), + )) + } + + Ok(expected_balances) +} + +/// A simple function to build the (stargate -> authz -> bank) /cosmos.bank.v1beta1.MsgSend message. +fn get_stargate_authz_bank_send_msg( + env: Env, + config: Config, + borrower: Addr, + amount: Vec, +) -> CosmosMsg { + // First we create the bank MsgSend + let bank_send_msg = MsgSend { + from_address: config.source.to_string(), + to_address: borrower.to_string(), + amount: amount + .iter() + .map(|x| ProtoCoin { + denom: x.clone().denom, + amount: x.amount.to_string(), + }) + .collect(), + }; + + // Then we wrap it in an authz message + let authz_msg_exec = MsgExec { + grantee: env.contract.address.to_string(), + msgs: vec![Any { + type_url: BANK_MSG_SEND_TYPE_URL.to_string(), + value: bank_send_msg.encode_to_vec(), + }], + }; + + // Then we wrap the authz message in a stargate message, because there is + // no custom support for authz messages in CosmWasm. + let stargate_authz_msg_exec: CosmosMsg = CosmosMsg::Stargate { + type_url: AUTHZ_MSG_EXEC_TYPE_URL.to_string(), + value: Binary(authz_msg_exec.encode_to_vec()), + }; + + stargate_authz_msg_exec +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + AUTHZ_BANK_SEND_REPLY_ID => { + // If we are here, the money is already on the borrower's account. This means that + // we can proceed to call the borrower's handler. + + let config: Config = CONFIG.load(deps.storage)?; + let active_loan = must_get_active_loan(&deps)?; + + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: active_loan.borrower.to_string(), + msg: to_json_binary(&BorrowerInterface::ProcessLoan { + // We sent the return address to the source address + return_address: config.source, + loan_amount: active_loan.amount, + fee: active_loan.fee, + }) + .unwrap(), + funds: vec![], + }); + // We use the reply_on_success strategy (we want the transaction to be simply reverted + // in case of an error). + Ok(Response::new() + .add_submessage(SubMsg::reply_on_success(msg, BORROWER_HANDLER_REPLY_ID)) + .add_attribute("action", "reply_authz_bank_send")) + } + BORROWER_HANDLER_REPLY_ID => { + // If we are here, the borrower smart contract has successfully executed the + // ProcessLoan message, and probably returned the funds to our contract together with + // the additional fee. + + let config: Config = CONFIG.load(deps.storage)?; + let active_loan = must_get_active_loan(&deps)?; + + // Check that the borrower returned the loan and paid the fee + check_expected_balances(deps.as_ref(), config.source, active_loan.expected_balances)?; + + // Set the active loan to None, thus making ourselves ready for the next loan + ACTIVE_LOAN.save(deps.storage, &None)?; + + Ok(Response::new().add_attribute("action", "reply_borrower_handler")) + } + _ => Err(ContractError::UnknownReplyID { id: msg.id }), + } +} + +fn check_expected_balances( + deps: Deps, + source: Addr, + expected_balances: Vec, +) -> Result<(), ContractError> { + // Prepare the query + let all_balances_query = BankQuery::AllBalances { + address: source.to_string(), + }; + // Get the response (all balances) + let all_balances_response: AllBalanceResponse = + deps.querier.query(&all_balances_query.into())?; + + // For each of the expected coin balances, check that the current balance of the source + // matches the expectations. We require **exactly** the expected amount (loan amount + fee) + // to be transferred back to the source, not more, not less. + for expected_coin in expected_balances { + let maybe_actual_balance = all_balances_response + .amount + .iter() + .find(|x| x.denom == expected_coin.denom && expected_coin.amount.eq(&x.amount)); + + if maybe_actual_balance.is_none() { + return Err(ContractError::IncorrectPayback {}); + } + } + + Ok(()) +} + +// Loads the information about the current loan, returns an error if the information +// is missing. +fn must_get_active_loan(deps: &DepsMut) -> Result { + let active_loan = ACTIVE_LOAN.load(deps.storage)?; + if active_loan.is_none() { + return Err(ContractError::UnexpectedState {}); + } + + Ok(active_loan.unwrap().clone()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_json_binary(&query_config(deps)?), + } +} + +pub fn query_config(deps: Deps) -> StdResult { + CONFIG.load(deps.storage) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} diff --git a/contracts/dao/neutron-flashloans/src/error.rs b/contracts/dao/neutron-flashloans/src/error.rs new file mode 100644 index 00000000..5724e5fe --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/error.rs @@ -0,0 +1,49 @@ +use cosmwasm_std::{CheckedMultiplyRatioError, OverflowError, StdError}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + // This error is returned if you requested a specific coin several times. + #[error("Duplicate denoms requested")] + DuplicateDenoms {}, + + // This error is returned if you requested zero of a specific coin. + #[error("Zero amount of {denom} requested")] + ZeroRequested { denom: String }, + + // This error is returned when you try to get a flashloan when you already + // have one. + #[error("A flashloan is already active in this transaction")] + FlashloanAlreadyActive {}, + + // This error is returned if the SOURCE doesn't have the requested amount of + // one of the requested assets. + #[error("Source doesn't have enough {denom}")] + InsufficientFunds { denom: String }, + + // This error is returned if the borrower did not return **exactly** the loan plus the fee + // to the source. + #[error("Borrower did not return exactly (loan + fee)")] + IncorrectPayback {}, + + // This error is returned if the contract received an unknown reply ID. + #[error("Unknown reply id: {id}")] + UnknownReplyID { id: u64 }, + + // This error is returned when we can't find the active loan information in our + // reply handlers. It's not supposed to occur at all. + #[error("Unexpected state: can't find active loan information")] + UnexpectedState {}, + + #[error("CheckedMultiplyRatioError error: {0}")] + CheckedMultiplyRatioError(#[from] CheckedMultiplyRatioError), + + #[error("OverflowError error: {0}")] + OverflowError(#[from] OverflowError), +} diff --git a/contracts/dao/neutron-flashloans/src/lib.rs b/contracts/dao/neutron-flashloans/src/lib.rs new file mode 100644 index 00000000..23cbb03f --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/lib.rs @@ -0,0 +1,6 @@ +pub mod contract; +mod error; +pub mod msg; +pub mod state; +#[cfg(test)] +mod testing; diff --git a/contracts/dao/neutron-flashloans/src/msg.rs b/contracts/dao/neutron-flashloans/src/msg.rs new file mode 100644 index 00000000..3691392a --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/msg.rs @@ -0,0 +1,61 @@ +use crate::state::Config; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin, Decimal}; + +#[cw_serde] +#[serde(rename_all = "snake_case")] +pub struct InstantiateMsg { + /// Defines the owner of the contract. The owner can add and remove + /// sources from the contract. + pub owner: Addr, + /// Defines the address that is going to sponsor the loans. This address + /// needs to grant a (Generic)Authorization to this contract to execute + /// /cosmos.bank.v1beta1.MsgSend on its behalf. + pub source: Addr, + /// Defines the fee rate for the loans, e.g., fee_rate = 0.01 + /// means that if you borrow 100untrn, you'll need to return 101untrn. + pub fee_rate: Decimal, +} + +#[cw_serde] +pub enum ExecuteMsg { + RequestLoan { + /// The amount that the borrower contract requests; there should be no + /// duplicate denoms and no zero amounts. + amount: Vec, + }, + UpdateConfig { + owner: Option, + fee_rate: Option, + source: Option, + }, +} + +/// Defines the interface for the borrowing contract. The borrowing +/// contract is required to implement a handler for the ProcessLoan message, +/// and to return loan_amount + fee to the return_address after executing its +/// custom logic, otherwise an error will be raised, and the whole transaction +/// will be rolled back. +#[cw_serde] +pub enum BorrowerInterface { + ProcessLoan { + /// Specifies the address to which the borrower must return the loan amount AND pay the fees. + return_address: Addr, + /// Specifies the loan amount which the borrower must return to the return_address. + loan_amount: Vec, + /// Specifies the fee which the borrower must pay to the return_address. + fee: Vec, + }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the current config value. + #[returns(Config)] + Config {}, +} + +#[cw_serde] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} diff --git a/contracts/dao/neutron-flashloans/src/state.rs b/contracts/dao/neutron-flashloans/src/state.rs new file mode 100644 index 00000000..b487b3be --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/state.rs @@ -0,0 +1,35 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin, Decimal}; +use cw_storage_plus::Item; + +pub const CONFIG: Item = Item::new("neutron-flashloans-config"); + +/// ACTIVE_LOAN keeps the information about the current loan, and acts as a reentrancy guard. +pub const ACTIVE_LOAN: Item> = Item::new("neutron-flashloans-active-loan"); + +#[cw_serde] +pub struct Config { + /// Defines the address that can modify the configuration of the contract. + pub owner: Addr, + /// Defines the fee rate for the loans, e.g., fee_rate = 0.01 + /// means that if you borrow 100untrn, you'll need to return 101untrn. + pub fee_rate: Decimal, + /// Defines the address that is going to sponsor the loans. This address + /// needs to grant a (Generic)Authorization to this contract to execute + /// /cosmos.bank.v1beta1.MsgSend on its behalf. + pub source: Addr, +} + +/// Defines the information that we store about the active loan. +#[cw_serde] +pub struct ActiveLoan { + /// The borrowing contract. + pub borrower: Addr, + /// The amount that was requested by the borrowing contract. + pub amount: Vec, + /// The fee that needs to be returned by the borrowing contract. + pub fee: Vec, + /// The expected balances of the source contract after the borrowing contract + /// returns (loan amount + fees). + pub expected_balances: Vec, +} diff --git a/contracts/dao/neutron-flashloans/src/testing/mock_querier.rs b/contracts/dao/neutron-flashloans/src/testing/mock_querier.rs new file mode 100644 index 00000000..c12d764c --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/testing/mock_querier.rs @@ -0,0 +1,48 @@ +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{ + from_json, Empty, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, +}; +use std::marker::PhantomData; + +pub fn mock_dependencies() -> OwnedDeps { + let custom_storage = MockStorage::default(); + let custom_querier = WasmMockQuerier::new(MockQuerier::new(&[])); + + OwnedDeps { + storage: custom_storage, + api: MockApi::default(), + querier: custom_querier, + custom_query_type: PhantomData, + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return QuerierResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }); + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + self.base.handle_query(request) + } +} + +impl WasmMockQuerier { + fn new(base: MockQuerier) -> WasmMockQuerier { + WasmMockQuerier { base } + } +} diff --git a/contracts/dao/neutron-flashloans/src/testing/mod.rs b/contracts/dao/neutron-flashloans/src/testing/mod.rs new file mode 100644 index 00000000..a1e507b6 --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock_querier; +mod tests; diff --git a/contracts/dao/neutron-flashloans/src/testing/tests.rs b/contracts/dao/neutron-flashloans/src/testing/tests.rs new file mode 100644 index 00000000..88d5616a --- /dev/null +++ b/contracts/dao/neutron-flashloans/src/testing/tests.rs @@ -0,0 +1,65 @@ +use crate::contract::{execute_request_loan, instantiate}; +use crate::error::ContractError; +use crate::msg::InstantiateMsg; +use crate::testing::mock_querier::mock_dependencies; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{Addr, Coin, Decimal}; + +#[test] +fn test_instantiate() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("addr1", &[]); + + instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + InstantiateMsg { + owner: Addr::unchecked("neutron_dao_address".to_string()), + source: Addr::unchecked("neutron_dao_address".to_string()), + fee_rate: Decimal::from_ratio(1u128, 100u128), + }, + ) + .unwrap(); +} + +#[test] +fn test_request_loan_invalid_amount() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("addr1", &[]); + + instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + InstantiateMsg { + owner: Addr::unchecked("neutron_dao_address".to_string()), + source: Addr::unchecked("neutron_dao_address".to_string()), + fee_rate: Decimal::from_ratio(1u128, 100u128), + }, + ) + .unwrap(); + + // ------------------------------- Duplicate denoms + let amount_duplicate_coins = vec![Coin::new(10u128, "untrn"), Coin::new(10u128, "untrn")]; + let err = execute_request_loan( + deps.as_mut(), + env.clone(), + info.clone(), + amount_duplicate_coins, + ) + .unwrap_err(); + assert_eq!(err, ContractError::DuplicateDenoms {}); + + // ------------------------------- Zero coins + let amount_duplicate_coins = vec![Coin::new(10u128, "untrn"), Coin::new(0u128, "uatom")]; + let err = execute_request_loan(deps.as_mut(), env, info, amount_duplicate_coins).unwrap_err(); + assert_eq!( + err, + ContractError::ZeroRequested { + denom: "uatom".to_string() + } + ) +} diff --git a/contracts/tokenomics/reserve/schema/neutron-reserve.json b/contracts/tokenomics/reserve/schema/neutron-reserve.json index 1b1917e3..e7367431 100644 --- a/contracts/tokenomics/reserve/schema/neutron-reserve.json +++ b/contracts/tokenomics/reserve/schema/neutron-reserve.json @@ -1,6 +1,6 @@ { "contract_name": "neutron-reserve", - "contract_version": "0.1.2", + "contract_version": "0.2.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#",