From 310e327fefcd6e8727cbb6168f039c80b6d657b8 Mon Sep 17 00:00:00 2001 From: emidev98 Date: Mon, 8 Jan 2024 17:52:32 +0200 Subject: [PATCH] init: alliance lp hub --- .gitignore | 2 + Cargo.lock | 18 + Cargo.toml | 1 + contracts/alliance-hub/README.md | 106 --- contracts/alliance-hub/examples/schema.rs | 2 +- .../alliance-hub/schema/alliance-hub.json | 2 +- contracts/alliance-hub/src/contract.rs | 15 +- contracts/alliance-hub/src/lib.rs | 3 +- contracts/alliance-hub/src/models.rs | 112 ++++ contracts/alliance-hub/src/query.rs | 8 +- contracts/alliance-hub/src/state.rs | 4 +- contracts/alliance-hub/src/tests/alliance.rs | 5 +- contracts/alliance-hub/src/tests/assets.rs | 4 +- contracts/alliance-hub/src/tests/helpers.rs | 9 +- .../alliance-hub/src/tests/instantiate.rs | 4 +- contracts/alliance-hub/src/tests/rewards.rs | 3 +- .../alliance-hub/src/tests/stake_unstake.rs | 4 +- contracts/alliance-lp-hub/.cargo/config | 5 + contracts/alliance-lp-hub/Cargo.toml | 32 + contracts/alliance-lp-hub/examples/schema.rs | 10 + contracts/alliance-lp-hub/src/contract.rs | 609 ++++++++++++++++++ contracts/alliance-lp-hub/src/lib.rs | 7 + contracts/alliance-lp-hub/src/models.rs | 108 ++++ contracts/alliance-lp-hub/src/query.rs | 163 +++++ contracts/alliance-lp-hub/src/state.rs | 24 + .../alliance-lp-hub/src/tests/helpers.rs | 17 + .../alliance-lp-hub/src/tests/instantiate.rs | 113 ++++ contracts/alliance-lp-hub/src/tests/mod.rs | 2 + contracts/alliance-oracle/src/contract.rs | 12 +- contracts/alliance-oracle/src/error.rs | 11 - contracts/alliance-oracle/src/lib.rs | 1 - contracts/alliance-oracle/src/utils.rs | 3 +- .../src/alliance_protocol.rs | 112 +--- .../alliance-protocol}/src/error.rs | 0 packages/alliance-protocol/src/lib.rs | 2 + .../alliance-protocol}/src/token_factory.rs | 0 36 files changed, 1273 insertions(+), 260 deletions(-) delete mode 100644 contracts/alliance-hub/README.md create mode 100644 contracts/alliance-hub/src/models.rs create mode 100644 contracts/alliance-lp-hub/.cargo/config create mode 100644 contracts/alliance-lp-hub/Cargo.toml create mode 100644 contracts/alliance-lp-hub/examples/schema.rs create mode 100644 contracts/alliance-lp-hub/src/contract.rs create mode 100644 contracts/alliance-lp-hub/src/lib.rs create mode 100644 contracts/alliance-lp-hub/src/models.rs create mode 100644 contracts/alliance-lp-hub/src/query.rs create mode 100644 contracts/alliance-lp-hub/src/state.rs create mode 100644 contracts/alliance-lp-hub/src/tests/helpers.rs create mode 100644 contracts/alliance-lp-hub/src/tests/instantiate.rs create mode 100644 contracts/alliance-lp-hub/src/tests/mod.rs delete mode 100644 contracts/alliance-oracle/src/error.rs rename {contracts/alliance-hub => packages/alliance-protocol}/src/error.rs (100%) rename {contracts/alliance-hub => packages/alliance-protocol}/src/token_factory.rs (100%) diff --git a/.gitignore b/.gitignore index 0121075..e0005d6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ node_modules .idea .env +.vscode + # Metadata from deployments scripts/.oracle_address.log scripts/.hub_address.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 26f9d9e..b4ea724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "alliance-lp-hub" +version = "0.1.0" +dependencies = [ + "alliance-protocol", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-asset", + "cw-storage-plus 0.16.0", + "cw-utils 1.0.1", + "cw2 1.0.1", + "schemars", + "serde", + "terra-proto-rs 3.0.2", + "thiserror", +] + [[package]] name = "alliance-oracle" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index bc67395..aaf38a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "contracts/alliance-hub", "contracts/alliance-oracle", + "contracts/alliance-lp-hub", "packages/alliance-protocol", ] exclude = [] diff --git a/contracts/alliance-hub/README.md b/contracts/alliance-hub/README.md deleted file mode 100644 index 954383a..0000000 --- a/contracts/alliance-hub/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# CosmWasm Starter Pack - -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. - -## Creating a new repo from template - -Assuming you have a recent version of rust and cargo (v1.58.1+) installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: - -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: - -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` - -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: - - -**Latest: 1.0.0-beta6** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -```` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -```` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. - -## Gitpod integration - -[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. - -Workspace contains: - - **rust**: for builds - - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client - - **jq**: shell JSON manipulation tool - -Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. - diff --git a/contracts/alliance-hub/examples/schema.rs b/contracts/alliance-hub/examples/schema.rs index b2a6544..65142cf 100644 --- a/contracts/alliance-hub/examples/schema.rs +++ b/contracts/alliance-hub/examples/schema.rs @@ -1,4 +1,4 @@ -use alliance_protocol::alliance_protocol::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use alliance_hub::models::{ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_schema::write_api; fn main() { diff --git a/contracts/alliance-hub/schema/alliance-hub.json b/contracts/alliance-hub/schema/alliance-hub.json index 1b00253..75afc0d 100644 --- a/contracts/alliance-hub/schema/alliance-hub.json +++ b/contracts/alliance-hub/schema/alliance-hub.json @@ -1,6 +1,6 @@ { "contract_name": "alliance-hub", - "contract_version": "0.1.0", + "contract_version": "0.1.1", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/alliance-hub/src/contract.rs b/contracts/alliance-hub/src/contract.rs index f2c2940..2bfeb1f 100644 --- a/contracts/alliance-hub/src/contract.rs +++ b/contracts/alliance-hub/src/contract.rs @@ -3,8 +3,8 @@ use cosmwasm_std::entry_point; use alliance_protocol::alliance_oracle_types::QueryMsg as OracleQueryMsg; use alliance_protocol::alliance_protocol::{ - AllianceDelegateMsg, AllianceRedelegateMsg, AllianceUndelegateMsg, AssetDistribution, Config, - ExecuteMsg, InstantiateMsg, MigrateMsg, + AllianceDelegateMsg, AllianceRedelegateMsg, AllianceUndelegateMsg, AssetDistribution, + MigrateMsg, }; use cosmwasm_std::{ to_json_binary, Addr, Binary, Coin as CwCoin, CosmosMsg, Decimal, DepsMut, Empty, Env, @@ -15,20 +15,22 @@ use cw2::set_contract_version; use cw_asset::{Asset, AssetInfo, AssetInfoBase, AssetInfoKey, AssetInfoUnchecked}; use cw_utils::parse_instantiate_response_data; use std::collections::{HashMap, HashSet}; - -use alliance_protocol::alliance_oracle_types::{AssetStaked, ChainId, EmissionsDistribution}; +use alliance_protocol::{ + alliance_oracle_types::{AssetStaked, ChainId, EmissionsDistribution}, + token_factory::{CustomExecuteMsg, DenomUnit, Metadata, TokenExecuteMsg}, + error::ContractError, +}; use terra_proto_rs::alliance::alliance::{ MsgClaimDelegationRewards, MsgDelegate, MsgRedelegate, MsgUndelegate, }; use terra_proto_rs::cosmos::base::v1beta1::Coin; use terra_proto_rs::traits::Message; -use crate::error::ContractError; +use crate::models::{InstantiateMsg, Config, ExecuteMsg}; use crate::state::{ ASSET_REWARD_DISTRIBUTION, ASSET_REWARD_RATE, BALANCES, CONFIG, TEMP_BALANCE, TOTAL_BALANCES, UNCLAIMED_REWARDS, USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST, }; -use crate::token_factory::{CustomExecuteMsg, DenomUnit, Metadata, TokenExecuteMsg}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:terra-alliance-protocol"; @@ -94,6 +96,7 @@ pub fn execute( ExecuteMsg::AllianceDelegate(msg) => alliance_delegate(deps, env, info, msg), ExecuteMsg::AllianceUndelegate(msg) => alliance_undelegate(deps, env, info, msg), ExecuteMsg::AllianceRedelegate(msg) => alliance_redelegate(deps, env, info, msg), + ExecuteMsg::UpdateRewards {} => update_rewards(deps, env, info), ExecuteMsg::RebalanceEmissions {} => rebalance_emissions(deps, env, info), diff --git a/contracts/alliance-hub/src/lib.rs b/contracts/alliance-hub/src/lib.rs index cf34250..e2a8ca8 100644 --- a/contracts/alliance-hub/src/lib.rs +++ b/contracts/alliance-hub/src/lib.rs @@ -1,7 +1,6 @@ pub mod contract; -pub mod error; pub mod query; pub mod state; +pub mod models; #[cfg(test)] mod tests; -mod token_factory; diff --git a/contracts/alliance-hub/src/models.rs b/contracts/alliance-hub/src/models.rs new file mode 100644 index 0000000..fe0f49e --- /dev/null +++ b/contracts/alliance-hub/src/models.rs @@ -0,0 +1,112 @@ +use std::collections::{HashMap,HashSet}; +use alliance_protocol::{ + alliance_oracle_types::ChainId, alliance_protocol::{ + AllianceDelegateMsg, + AllianceUndelegateMsg, + AllianceRedelegateMsg, + AssetDistribution + }, +}; +use cosmwasm_schema::{QueryResponses, cw_serde}; +use cosmwasm_std::{Addr, Timestamp, Uint128}; +use cw_asset::{AssetInfo, Asset}; + + +#[cw_serde] +pub struct Config { + pub governance: Addr, + pub controller: Addr, + pub oracle: Addr, + pub last_reward_update_timestamp: Timestamp, + pub alliance_token_denom: String, + pub alliance_token_supply: Uint128, + pub reward_denom: String, +} + +#[cw_serde] +pub struct InstantiateMsg { + pub governance: String, + pub controller: String, + pub oracle: String, + pub reward_denom: String, +} + +#[cw_serde] +pub enum ExecuteMsg { + // Public functions + Stake {}, + Unstake(Asset), + ClaimRewards(AssetInfo), + UpdateRewards {}, + + // Privileged functions + WhitelistAssets(HashMap>), + RemoveAssets(Vec), + UpdateRewardsCallback {}, + AllianceDelegate(AllianceDelegateMsg), + AllianceUndelegate(AllianceUndelegateMsg), + AllianceRedelegate(AllianceRedelegateMsg), + RebalanceEmissions {}, + RebalanceEmissionsCallback {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Config)] + Config {}, + + #[returns(HashSet)] + Validators {}, + + #[returns(WhitelistedAssetsResponse)] + WhitelistedAssets {}, + + #[returns(Vec)] + RewardDistribution {}, + + #[returns(StakedBalanceRes)] + StakedBalance(AssetQuery), + + #[returns(PendingRewardsRes)] + PendingRewards(AssetQuery), + + #[returns(Vec)] + AllStakedBalances(AllStakedBalancesQuery), + + #[returns(Vec)] + AllPendingRewards(AllPendingRewardsQuery), + + #[returns(Vec)] + TotalStakedBalances {}, +} +pub type WhitelistedAssetsResponse = HashMap>; + +#[cw_serde] +pub struct AllPendingRewardsQuery { + pub address: String, +} + +#[cw_serde] +pub struct AllStakedBalancesQuery { + pub address: String, +} + +#[cw_serde] +pub struct PendingRewardsRes { + pub staked_asset: AssetInfo, + pub reward_asset: AssetInfo, + pub rewards: Uint128, +} + +#[cw_serde] +pub struct AssetQuery { + pub address: String, + pub asset: AssetInfo, +} + +#[cw_serde] +pub struct StakedBalanceRes { + pub asset: AssetInfo, + pub balance: Uint128, +} diff --git a/contracts/alliance-hub/src/query.rs b/contracts/alliance-hub/src/query.rs index c9deaab..257fcef 100644 --- a/contracts/alliance-hub/src/query.rs +++ b/contracts/alliance-hub/src/query.rs @@ -1,17 +1,13 @@ -use alliance_protocol::alliance_protocol::{ - AllPendingRewardsQuery, AllStakedBalancesQuery, AssetQuery, PendingRewardsRes, QueryMsg, - StakedBalanceRes, WhitelistedAssetsResponse, -}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{to_json_binary, Binary, Deps, Env, Order, StdResult, Uint128}; use cw_asset::{AssetInfo, AssetInfoKey}; use std::collections::HashMap; -use crate::state::{ +use crate::{state::{ ASSET_REWARD_DISTRIBUTION, ASSET_REWARD_RATE, BALANCES, CONFIG, TOTAL_BALANCES, UNCLAIMED_REWARDS, USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST, -}; +}, models::{QueryMsg, WhitelistedAssetsResponse, StakedBalanceRes, AssetQuery, PendingRewardsRes, AllStakedBalancesQuery, AllPendingRewardsQuery}}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { diff --git a/contracts/alliance-hub/src/state.rs b/contracts/alliance-hub/src/state.rs index 44639c3..3a5b987 100644 --- a/contracts/alliance-hub/src/state.rs +++ b/contracts/alliance-hub/src/state.rs @@ -1,10 +1,12 @@ use alliance_protocol::alliance_oracle_types::ChainId; -use alliance_protocol::alliance_protocol::{AssetDistribution, Config}; +use alliance_protocol::alliance_protocol::AssetDistribution; use cosmwasm_std::{Addr, Decimal, Uint128}; use cw_asset::AssetInfoKey; use cw_storage_plus::{Item, Map}; use std::collections::HashSet; +use crate::models::Config; + pub const CONFIG: Item = Item::new("config"); pub const WHITELIST: Map = Map::new("whitelist"); diff --git a/contracts/alliance-hub/src/tests/alliance.rs b/contracts/alliance-hub/src/tests/alliance.rs index cd78e5a..847168f 100644 --- a/contracts/alliance-hub/src/tests/alliance.rs +++ b/contracts/alliance-hub/src/tests/alliance.rs @@ -1,11 +1,12 @@ use crate::contract::execute; -use crate::error::ContractError; +use crate::models::{Config, ExecuteMsg}; +use alliance_protocol::error::ContractError; use crate::state::{CONFIG, VALIDATORS}; use crate::tests::helpers::{ alliance_delegate, alliance_redelegate, alliance_undelegate, setup_contract, }; use alliance_protocol::alliance_protocol::{ - AllianceDelegateMsg, AllianceDelegation, AllianceUndelegateMsg, Config, ExecuteMsg, + AllianceDelegateMsg, AllianceDelegation, AllianceUndelegateMsg }; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{Binary, CosmosMsg, StdResult, SubMsg, Uint128}; diff --git a/contracts/alliance-hub/src/tests/assets.rs b/contracts/alliance-hub/src/tests/assets.rs index 92461c1..0f0d765 100644 --- a/contracts/alliance-hub/src/tests/assets.rs +++ b/contracts/alliance-hub/src/tests/assets.rs @@ -1,9 +1,9 @@ use crate::contract::execute; -use crate::error::ContractError; +use alliance_protocol::error::ContractError; use crate::query::query; use crate::state::WHITELIST; use crate::tests::helpers::{remove_assets, setup_contract, whitelist_assets}; -use alliance_protocol::alliance_protocol::{ExecuteMsg, QueryMsg, WhitelistedAssetsResponse}; +use crate::models::{ExecuteMsg, QueryMsg, WhitelistedAssetsResponse}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{from_json, Response}; use cw_asset::{AssetInfo, AssetInfoKey}; diff --git a/contracts/alliance-hub/src/tests/helpers.rs b/contracts/alliance-hub/src/tests/helpers.rs index 628625b..1b20703 100644 --- a/contracts/alliance-hub/src/tests/helpers.rs +++ b/contracts/alliance-hub/src/tests/helpers.rs @@ -1,11 +1,14 @@ use crate::contract::{execute, instantiate}; use crate::query::query; use crate::state::CONFIG; -use crate::token_factory::CustomExecuteMsg; +use alliance_protocol::token_factory::CustomExecuteMsg; use alliance_protocol::alliance_oracle_types::ChainId; use alliance_protocol::alliance_protocol::{ - AllPendingRewardsQuery, AllianceDelegateMsg, AllianceDelegation, AllianceRedelegateMsg, - AllianceRedelegation, AllianceUndelegateMsg, AssetQuery, Config, ExecuteMsg, InstantiateMsg, + AllianceDelegateMsg, AllianceDelegation, AllianceRedelegateMsg, + AllianceRedelegation, AllianceUndelegateMsg, +}; +use crate::models::{ + AllPendingRewardsQuery, AssetQuery, Config, ExecuteMsg, InstantiateMsg, PendingRewardsRes, QueryMsg, StakedBalanceRes, }; use cosmwasm_std::testing::{mock_env, mock_info}; diff --git a/contracts/alliance-hub/src/tests/instantiate.rs b/contracts/alliance-hub/src/tests/instantiate.rs index 2e4dfa9..9e06d51 100644 --- a/contracts/alliance-hub/src/tests/instantiate.rs +++ b/contracts/alliance-hub/src/tests/instantiate.rs @@ -1,8 +1,8 @@ use crate::contract::reply; use crate::query::query; use crate::tests::helpers::setup_contract; -use crate::token_factory::{CustomExecuteMsg, DenomUnit, Metadata, TokenExecuteMsg}; -use alliance_protocol::alliance_protocol::{Config, QueryMsg}; +use alliance_protocol::token_factory::{CustomExecuteMsg, DenomUnit, Metadata, TokenExecuteMsg}; +use crate::models::{Config, QueryMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env}; use cosmwasm_std::{ from_json, Addr, Binary, CosmosMsg, Reply, Response, SubMsg, SubMsgResponse, SubMsgResult, diff --git a/contracts/alliance-hub/src/tests/rewards.rs b/contracts/alliance-hub/src/tests/rewards.rs index b2d9b6e..cbfb23e 100644 --- a/contracts/alliance-hub/src/tests/rewards.rs +++ b/contracts/alliance-hub/src/tests/rewards.rs @@ -7,7 +7,8 @@ use crate::tests::helpers::{ claim_rewards, query_all_rewards, query_rewards, set_alliance_asset, setup_contract, stake, unstake, whitelist_assets, DENOM, }; -use alliance_protocol::alliance_protocol::{AssetDistribution, ExecuteMsg, PendingRewardsRes}; +use alliance_protocol::alliance_protocol::AssetDistribution; +use crate::models::{ExecuteMsg, PendingRewardsRes}; use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; use cosmwasm_std::{ coin, coins, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Decimal, Response, SubMsg, diff --git a/contracts/alliance-hub/src/tests/stake_unstake.rs b/contracts/alliance-hub/src/tests/stake_unstake.rs index 2c81172..fbb17ba 100644 --- a/contracts/alliance-hub/src/tests/stake_unstake.rs +++ b/contracts/alliance-hub/src/tests/stake_unstake.rs @@ -1,10 +1,10 @@ use crate::contract::execute; -use crate::error::ContractError; +use alliance_protocol::error::ContractError; use crate::state::{BALANCES, TOTAL_BALANCES}; use crate::tests::helpers::{ query_all_staked_balances, setup_contract, stake, unstake, whitelist_assets, }; -use alliance_protocol::alliance_protocol::{ExecuteMsg, StakedBalanceRes}; +use crate::models::{ExecuteMsg, StakedBalanceRes}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Response, Uint128}; use cw_asset::{Asset, AssetInfo, AssetInfoKey}; diff --git a/contracts/alliance-lp-hub/.cargo/config b/contracts/alliance-lp-hub/.cargo/config new file mode 100644 index 0000000..cc2a25b --- /dev/null +++ b/contracts/alliance-lp-hub/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" \ No newline at end of file diff --git a/contracts/alliance-lp-hub/Cargo.toml b/contracts/alliance-lp-hub/Cargo.toml new file mode 100644 index 0000000..bd19f2f --- /dev/null +++ b/contracts/alliance-lp-hub/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "alliance-lp-hub" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2018" + +exclude = [ + "contract.wasm", + "hash.txt", +] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-std = { workspace = true, features = ["stargate"] } +cosmwasm-storage = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-asset = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +alliance-protocol = { workspace = true } + +cw2 = "1.0.1" +cw-utils = "1.0.1" +terra-proto-rs = {version = "3.0.2", default-features = false} diff --git a/contracts/alliance-lp-hub/examples/schema.rs b/contracts/alliance-lp-hub/examples/schema.rs new file mode 100644 index 0000000..269e59c --- /dev/null +++ b/contracts/alliance-lp-hub/examples/schema.rs @@ -0,0 +1,10 @@ +use alliance_lp_hub::models::{InstantiateMsg, ExecuteMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/alliance-lp-hub/src/contract.rs b/contracts/alliance-lp-hub/src/contract.rs new file mode 100644 index 0000000..b0864d6 --- /dev/null +++ b/contracts/alliance-lp-hub/src/contract.rs @@ -0,0 +1,609 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use alliance_protocol::{ + token_factory::{CustomExecuteMsg, DenomUnit, Metadata, TokenExecuteMsg}, + alliance_protocol::{ + AllianceDelegateMsg, AllianceRedelegateMsg, AllianceUndelegateMsg, MigrateMsg, + }, + error::ContractError, alliance_oracle_types::ChainId, +}; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, Coin as CwCoin, CosmosMsg, Decimal, DepsMut, Empty, Env, + MessageInfo, Reply, Response, StdError, StdResult, Storage, SubMsg, Uint128, WasmMsg, +}; +use cw2::set_contract_version; +use cw_asset::{Asset, AssetInfo, AssetInfoKey}; +use cw_utils::parse_instantiate_response_data; +use std::collections::{HashMap, HashSet}; +use terra_proto_rs::{ + alliance::alliance::{ + MsgClaimDelegationRewards, MsgDelegate, MsgRedelegate, MsgUndelegate, + }, + cosmos::base::v1beta1::Coin, + traits::Message, +}; + +use crate::{ + state::{ + ASSET_REWARD_DISTRIBUTION, ASSET_REWARD_RATE, BALANCES, + CONFIG, TEMP_BALANCE, TOTAL_BALANCES, UNCLAIMED_REWARDS, + USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST + }, + models::{ + Config, ExecuteMsg, InstantiateMsg + } +}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:terra-alliance-lp-hub"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +const CREATE_REPLY_ID: u64 = 1; +const CLAIM_REWARD_ERROR_REPLY_ID: u64 = 2; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result, ContractError> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let governance_address = deps.api.addr_validate(msg.governance.as_str())?; + let controller_address = deps.api.addr_validate(msg.controller.as_str())?; + let create_msg = TokenExecuteMsg::CreateDenom { + subdenom: "ualliancelp".to_string(), + }; + let sub_msg = SubMsg::reply_on_success( + CosmosMsg::Custom(CustomExecuteMsg::Token(create_msg)), + CREATE_REPLY_ID, + ); + let config = Config { + governance: governance_address, + controller: controller_address, + alliance_token_denom: "".to_string(), + alliance_token_supply: Uint128::zero(), + reward_denom: msg.reward_denom, + }; + CONFIG.save(deps.storage, &config)?; + + VALIDATORS.save(deps.storage, &HashSet::new())?; + Ok(Response::new() + .add_attributes(vec![("action", "instantiate")]) + .add_submessage(sub_msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::WhitelistAssets(assets) => whitelist_assets(deps, info, assets), + ExecuteMsg::RemoveAssets(assets) => remove_assets(deps, info, assets), + + ExecuteMsg::Stake {} => stake(deps, env, info), + ExecuteMsg::Unstake(asset) => unstake(deps, info, asset), + ExecuteMsg::ClaimRewards(asset) => claim_rewards(deps, info, asset), + + ExecuteMsg::AllianceDelegate(msg) => alliance_delegate(deps, env, info, msg), + ExecuteMsg::AllianceUndelegate(msg) => alliance_undelegate(deps, env, info, msg), + ExecuteMsg::AllianceRedelegate(msg) => alliance_redelegate(deps, env, info, msg), + + ExecuteMsg::UpdateRewards {} => update_rewards(deps, env, info), + ExecuteMsg::RebalanceEmissions {} => rebalance_emissions(deps, env, info), + + ExecuteMsg::UpdateRewardsCallback {} => update_reward_callback(deps, env, info), + ExecuteMsg::RebalanceEmissionsCallback {} => rebalance_emissions_callback(deps, env, info), + } +} + +fn whitelist_assets( + deps: DepsMut, + info: MessageInfo, + assets_request: HashMap>, +) -> Result { + let config = CONFIG.load(deps.storage)?; + is_governance(&info, &config)?; + let mut attrs = vec![("action".to_string(), "whitelist_assets".to_string())]; + for (chain_id, assets) in &assets_request { + for asset in assets { + let asset_key = AssetInfoKey::from(asset.clone()); + WHITELIST.save(deps.storage, asset_key.clone(), chain_id)?; + ASSET_REWARD_RATE.update(deps.storage, asset_key, |rate| -> StdResult<_> { + Ok(rate.unwrap_or(Decimal::zero())) + })?; + } + attrs.push(("chain_id".to_string(), chain_id.to_string())); + let assets_str = assets + .iter() + .map(|asset| asset.to_string()) + .collect::>() + .join(","); + + attrs.push(("assets".to_string(), assets_str.to_string())); + } + Ok(Response::new().add_attributes(attrs)) +} + +fn remove_assets( + deps: DepsMut, + info: MessageInfo, + assets: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + // Only allow the governance address to update whitelisted assets + is_governance(&info, &config)?; + for asset in &assets { + let asset_key = AssetInfoKey::from(asset.clone()); + WHITELIST.remove(deps.storage, asset_key); + } + let assets_str = assets + .iter() + .map(|asset| asset.to_string()) + .collect::>() + .join(","); + Ok(Response::new().add_attributes(vec![("action", "remove_assets"), ("assets", &assets_str)])) +} + +fn stake(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { + if info.funds.len() != 1 { + return Err(ContractError::OnlySingleAssetAllowed {}); + } + if info.funds[0].amount.is_zero() { + return Err(ContractError::AmountCannotBeZero {}); + } + let asset = AssetInfo::native(&info.funds[0].denom); + let asset_key = AssetInfoKey::from(&asset); + WHITELIST + .load(deps.storage, asset_key.clone()) + .map_err(|_| ContractError::AssetNotWhitelisted {})?; + let sender = info.sender.clone(); + + let rewards = _claim_reward(deps.storage, sender.clone(), asset.clone())?; + if !rewards.is_zero() { + UNCLAIMED_REWARDS.update( + deps.storage, + (sender.clone(), asset_key.clone()), + |balance| -> Result<_, ContractError> { + Ok(balance.unwrap_or(Uint128::zero()) + rewards) + }, + )?; + } + + BALANCES.update( + deps.storage, + (sender.clone(), asset_key.clone()), + |balance| -> Result<_, ContractError> { + match balance { + Some(balance) => Ok(balance + info.funds[0].amount), + None => Ok(info.funds[0].amount), + } + }, + )?; + TOTAL_BALANCES.update( + deps.storage, + asset_key.clone(), + |balance| -> Result<_, ContractError> { + Ok(balance.unwrap_or(Uint128::zero()) + info.funds[0].amount) + }, + )?; + + let asset_reward_rate = ASSET_REWARD_RATE + .load(deps.storage, asset_key.clone()) + .unwrap_or(Decimal::zero()); + USER_ASSET_REWARD_RATE.save(deps.storage, (sender, asset_key), &asset_reward_rate)?; + + Ok(Response::new().add_attributes(vec![ + ("action", "stake"), + ("user", info.sender.as_ref()), + ("asset", &asset.to_string()), + ("amount", &info.funds[0].amount.to_string()), + ])) +} + +fn unstake(deps: DepsMut, info: MessageInfo, asset: Asset) -> Result { + let asset_key = AssetInfoKey::from(asset.info.clone()); + let sender = info.sender.clone(); + if asset.amount.is_zero() { + return Err(ContractError::AmountCannotBeZero {}); + } + + let rewards = _claim_reward(deps.storage, sender.clone(), asset.info.clone())?; + if !rewards.is_zero() { + UNCLAIMED_REWARDS.update( + deps.storage, + (sender.clone(), asset_key.clone()), + |balance| -> Result<_, ContractError> { + Ok(balance.unwrap_or(Uint128::zero()) + rewards) + }, + )?; + } + + BALANCES.update( + deps.storage, + (sender, asset_key.clone()), + |balance| -> Result<_, ContractError> { + match balance { + Some(balance) => { + if balance < asset.amount { + return Err(ContractError::InsufficientBalance {}); + } + Ok(balance - asset.amount) + } + None => Err(ContractError::InsufficientBalance {}), + } + }, + )?; + TOTAL_BALANCES.update( + deps.storage, + asset_key, + |balance| -> Result<_, ContractError> { + let balance = balance.unwrap_or(Uint128::zero()); + if balance < asset.amount { + return Err(ContractError::InsufficientBalance {}); + } + Ok(balance - asset.amount) + }, + )?; + + let msg = asset.transfer_msg(&info.sender)?; + + Ok(Response::new() + .add_attributes(vec![ + ("action", "unstake"), + ("user", info.sender.as_ref()), + ("asset", &asset.info.to_string()), + ("amount", &asset.amount.to_string()), + ]) + .add_message(msg)) +} + +fn claim_rewards( + deps: DepsMut, + info: MessageInfo, + asset: AssetInfo, +) -> Result { + let user = info.sender; + let config = CONFIG.load(deps.storage)?; + let rewards = _claim_reward(deps.storage, user.clone(), asset.clone())?; + let unclaimed_rewards = UNCLAIMED_REWARDS + .load( + deps.storage, + (user.clone(), AssetInfoKey::from(asset.clone())), + ) + .unwrap_or(Uint128::zero()); + let final_rewards = rewards + unclaimed_rewards; + UNCLAIMED_REWARDS.remove( + deps.storage, + (user.clone(), AssetInfoKey::from(asset.clone())), + ); + let response = Response::new().add_attributes(vec![ + ("action", "claim_rewards"), + ("user", user.as_ref()), + ("asset", &asset.to_string()), + ("reward_amount", &final_rewards.to_string()), + ]); + if !final_rewards.is_zero() { + let rewards_asset = Asset { + info: AssetInfo::Native(config.reward_denom), + amount: final_rewards, + }; + Ok(response.add_message(rewards_asset.transfer_msg(&user)?)) + } else { + Ok(response) + } +} + +fn _claim_reward( + storage: &mut dyn Storage, + user: Addr, + asset: AssetInfo, +) -> Result { + let asset_key = AssetInfoKey::from(&asset); + let user_reward_rate = USER_ASSET_REWARD_RATE.load(storage, (user.clone(), asset_key.clone())); + let asset_reward_rate = ASSET_REWARD_RATE.load(storage, asset_key.clone())?; + + if let Ok(user_reward_rate) = user_reward_rate { + let user_staked = BALANCES.load(storage, (user.clone(), asset_key.clone()))?; + let rewards = ((asset_reward_rate - user_reward_rate) + * Decimal::from_atomics(user_staked, 0)?) + .to_uint_floor(); + if rewards.is_zero() { + Ok(Uint128::zero()) + } else { + USER_ASSET_REWARD_RATE.save(storage, (user, asset_key), &asset_reward_rate)?; + Ok(rewards) + } + } else { + // If cannot find user_reward_rate, assume this is the first time they are staking and set it to the current asset_reward_rate + USER_ASSET_REWARD_RATE.save(storage, (user, asset_key), &asset_reward_rate)?; + + Ok(Uint128::zero()) + } +} + +fn alliance_delegate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: AllianceDelegateMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + is_controller(&info, &config)?; + if msg.delegations.is_empty() { + return Err(ContractError::EmptyDelegation {}); + } + let mut validators = VALIDATORS.load(deps.storage)?; + let mut msgs: Vec> = vec![]; + for delegation in msg.delegations { + let delegate_msg = MsgDelegate { + amount: Some(Coin { + denom: config.alliance_token_denom.clone(), + amount: delegation.amount.to_string(), + }), + delegator_address: env.contract.address.to_string(), + validator_address: delegation.validator.to_string(), + }; + msgs.push(CosmosMsg::Stargate { + type_url: "/alliance.alliance.MsgDelegate".to_string(), + value: Binary::from(delegate_msg.encode_to_vec()), + }); + validators.insert(delegation.validator); + } + VALIDATORS.save(deps.storage, &validators)?; + Ok(Response::new() + .add_attributes(vec![("action", "alliance_delegate")]) + .add_messages(msgs)) +} + +fn alliance_undelegate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: AllianceUndelegateMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + is_controller(&info, &config)?; + if msg.undelegations.is_empty() { + return Err(ContractError::EmptyDelegation {}); + } + let mut msgs = vec![]; + for delegation in msg.undelegations { + let undelegate_msg = MsgUndelegate { + amount: Some(Coin { + denom: config.alliance_token_denom.clone(), + amount: delegation.amount.to_string(), + }), + delegator_address: env.contract.address.to_string(), + validator_address: delegation.validator.to_string(), + }; + let msg = CosmosMsg::Stargate { + type_url: "/alliance.alliance.MsgUndelegate".to_string(), + value: Binary::from(undelegate_msg.encode_to_vec()), + }; + msgs.push(msg); + } + Ok(Response::new() + .add_attributes(vec![("action", "alliance_undelegate")]) + .add_messages(msgs)) +} + +fn alliance_redelegate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: AllianceRedelegateMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + is_controller(&info, &config)?; + if msg.redelegations.is_empty() { + return Err(ContractError::EmptyDelegation {}); + } + let mut msgs = vec![]; + let mut validators = VALIDATORS.load(deps.storage)?; + for redelegation in msg.redelegations { + let src_validator = redelegation.src_validator; + let dst_validator = redelegation.dst_validator; + let redelegate_msg = MsgRedelegate { + amount: Some(Coin { + denom: config.alliance_token_denom.clone(), + amount: redelegation.amount.to_string(), + }), + delegator_address: env.contract.address.to_string(), + validator_src_address: src_validator.to_string(), + validator_dst_address: dst_validator.to_string(), + }; + let msg = CosmosMsg::Stargate { + type_url: "/alliance.alliance.MsgRedelegate".to_string(), + value: Binary::from(redelegate_msg.encode_to_vec()), + }; + msgs.push(msg); + validators.insert(dst_validator); + } + VALIDATORS.save(deps.storage, &validators)?; + Ok(Response::new() + .add_attributes(vec![("action", "alliance_redelegate")]) + .add_messages(msgs)) +} + +fn update_rewards(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + let config = CONFIG.load(deps.storage)?; + + let reward_sent_in_tx: Option<&CwCoin> = + info.funds.iter().find(|c| c.denom == config.reward_denom); + let sent_balance = if let Some(coin) = reward_sent_in_tx { + coin.amount + } else { + Uint128::zero() + }; + let reward_asset = AssetInfo::native(config.reward_denom.clone()); + let contract_balance = + reward_asset.query_balance(&deps.querier, env.contract.address.clone())?; + + // Contract balance is guaranteed to be greater than sent balance + // since contract balance = previous contract balance + sent balance > sent balance + TEMP_BALANCE.save(deps.storage, &(contract_balance - sent_balance))?; + let validators = VALIDATORS.load(deps.storage)?; + let sub_msgs: Vec = validators + .iter() + .map(|v| { + let msg = MsgClaimDelegationRewards { + delegator_address: env.contract.address.to_string(), + validator_address: v.to_string(), + denom: config.alliance_token_denom.clone(), + }; + let msg = CosmosMsg::Stargate { + type_url: "/alliance.alliance.MsgClaimDelegationRewards".to_string(), + value: Binary::from(msg.encode_to_vec()), + }; + // Reply on error here is used to ignore errors from claiming rewards with validators that we did not delegate to + SubMsg::reply_on_error(msg, CLAIM_REWARD_ERROR_REPLY_ID) + }) + .collect(); + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::UpdateRewardsCallback {}).unwrap(), + funds: vec![], + }); + + Ok(Response::new() + .add_attributes(vec![("action", "update_rewards")]) + .add_submessages(sub_msgs) + .add_message(msg)) +} + +fn update_reward_callback( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + let config = CONFIG.load(deps.storage)?; + + // TODO: maths + + Ok(Response::new().add_attributes(vec![("action", "update_rewards_callback")])) +} + +fn rebalance_emissions( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + // Allow execution only from the controller account + let config = CONFIG.load(deps.storage)?; + is_controller(&info, &config)?; + + // Before starting with the rebalance emission process + // rewards must be updated to the current block height + // Skip if no reward distribution in the first place + let res = if ASSET_REWARD_DISTRIBUTION.load(deps.storage).is_ok() { + update_rewards(deps, env.clone(), info)? + } else { + Response::new() + }; + + Ok(res.add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::RebalanceEmissionsCallback {}).unwrap(), + funds: vec![], + }))) +} + +fn rebalance_emissions_callback( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + // TODO maths + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply( + deps: DepsMut, + env: Env, + reply: Reply, +) -> Result, ContractError> { + match reply.id { + CREATE_REPLY_ID => { + let response = reply.result.unwrap(); + // It works because the response data is a protobuf encoded string that contains the denom in the first slot (similar to the contract instantiation response) + let denom = parse_instantiate_response_data(response.data.unwrap().as_slice()) + .map_err(|_| ContractError::Std(StdError::generic_err("parse error".to_string())))? + .contract_address; + let total_supply = Uint128::from(1_000_000_000_000_u128); + let sub_msg_mint = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( + TokenExecuteMsg::MintTokens { + denom: denom.clone(), + amount: total_supply, + mint_to_address: env.contract.address.to_string(), + }, + ))); + CONFIG.update(deps.storage, |mut config| -> Result<_, ContractError> { + config.alliance_token_denom = denom.clone(); + config.alliance_token_supply = total_supply; + Ok(config) + })?; + let symbol = "ALLIANCE_LP"; + + let sub_msg_metadata = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( + TokenExecuteMsg::SetMetadata { + denom: denom.clone(), + metadata: Metadata { + description: "Staking token for the alliance protocol lp contract".to_string(), + denom_units: vec![DenomUnit { + denom: denom.clone(), + exponent: 0, + aliases: vec![], + }], + base: denom.to_string(), + display: denom.to_string(), + name: "Alliance LP Token".to_string(), + symbol: symbol.to_string(), + }, + }, + ))); + Ok(Response::new() + .add_attributes(vec![ + ("alliance_token_denom", denom), + ("alliance_token_total_supply", total_supply.to_string()), + ]) + .add_submessage(sub_msg_mint) + .add_submessage(sub_msg_metadata)) + } + CLAIM_REWARD_ERROR_REPLY_ID => { + Ok(Response::new().add_attributes(vec![("action", "claim_reward_error")])) + } + _ => Err(ContractError::InvalidReplyId(reply.id)), + } +} + +// Controller is used to perform administrative operations that deals with delegating the virtual +// tokens to the expected validators +fn is_controller(info: &MessageInfo, config: &Config) -> Result<(), ContractError> { + if info.sender != config.controller { + return Err(ContractError::Unauthorized {}); + } + Ok(()) +} + +// Only governance (through a on-chain prop) can change the whitelisted assets +fn is_governance(info: &MessageInfo, config: &Config) -> Result<(), ContractError> { + if info.sender != config.governance { + return Err(ContractError::Unauthorized {}); + } + Ok(()) +} diff --git a/contracts/alliance-lp-hub/src/lib.rs b/contracts/alliance-lp-hub/src/lib.rs new file mode 100644 index 0000000..64466be --- /dev/null +++ b/contracts/alliance-lp-hub/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod query; +pub mod state; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/contracts/alliance-lp-hub/src/models.rs b/contracts/alliance-lp-hub/src/models.rs new file mode 100644 index 0000000..e9f7413 --- /dev/null +++ b/contracts/alliance-lp-hub/src/models.rs @@ -0,0 +1,108 @@ +use std::collections::{HashMap,HashSet}; +use alliance_protocol::{ + alliance_oracle_types::ChainId, alliance_protocol::{ + AllianceDelegateMsg, + AllianceUndelegateMsg, + AllianceRedelegateMsg, + AssetDistribution + }, +}; +use cosmwasm_schema::{QueryResponses, cw_serde}; +use cosmwasm_std::{Addr, Uint128}; +use cw_asset::{AssetInfo, Asset}; + +#[cw_serde] +pub struct Config { + pub governance: Addr, + pub controller: Addr, + pub alliance_token_denom: String, + pub alliance_token_supply: Uint128, + pub reward_denom: String, +} + +#[cw_serde] +pub struct InstantiateMsg { + pub governance: String, + pub controller: String, + pub reward_denom: String, +} + +#[cw_serde] +pub enum ExecuteMsg { + // Public functions + Stake {}, + Unstake(Asset), + ClaimRewards(AssetInfo), + UpdateRewards {}, + + // Privileged functions + WhitelistAssets(HashMap>), + RemoveAssets(Vec), + UpdateRewardsCallback {}, + AllianceDelegate(AllianceDelegateMsg), + AllianceUndelegate(AllianceUndelegateMsg), + AllianceRedelegate(AllianceRedelegateMsg), + RebalanceEmissions {}, + RebalanceEmissionsCallback {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Config)] + Config {}, + + #[returns(HashSet)] + Validators {}, + + #[returns(WhitelistedAssetsResponse)] + WhitelistedAssets {}, + + #[returns(Vec)] + RewardDistribution {}, + + #[returns(StakedBalanceRes)] + StakedBalance(AssetQuery), + + #[returns(PendingRewardsRes)] + PendingRewards(AssetQuery), + + #[returns(Vec)] + AllStakedBalances(AllStakedBalancesQuery), + + #[returns(Vec)] + AllPendingRewards(AllPendingRewardsQuery), + + #[returns(Vec)] + TotalStakedBalances {}, +} +pub type WhitelistedAssetsResponse = HashMap>; + +#[cw_serde] +pub struct AllPendingRewardsQuery { + pub address: String, +} + +#[cw_serde] +pub struct AllStakedBalancesQuery { + pub address: String, +} + +#[cw_serde] +pub struct PendingRewardsRes { + pub staked_asset: AssetInfo, + pub reward_asset: AssetInfo, + pub rewards: Uint128, +} + +#[cw_serde] +pub struct AssetQuery { + pub address: String, + pub asset: AssetInfo, +} + +#[cw_serde] +pub struct StakedBalanceRes { + pub asset: AssetInfo, + pub balance: Uint128, +} diff --git a/contracts/alliance-lp-hub/src/query.rs b/contracts/alliance-lp-hub/src/query.rs new file mode 100644 index 0000000..64926c0 --- /dev/null +++ b/contracts/alliance-lp-hub/src/query.rs @@ -0,0 +1,163 @@ +use crate::models::{ + AllPendingRewardsQuery, AllStakedBalancesQuery, AssetQuery, + PendingRewardsRes, QueryMsg, StakedBalanceRes, WhitelistedAssetsResponse, +}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_json_binary, Binary, Deps, Env, Order, StdResult, Uint128}; +use cw_asset::{AssetInfo, AssetInfoKey}; +use std::collections::HashMap; + +use crate::state::{ + ASSET_REWARD_DISTRIBUTION, ASSET_REWARD_RATE, BALANCES, CONFIG, TOTAL_BALANCES, + UNCLAIMED_REWARDS, USER_ASSET_REWARD_RATE, VALIDATORS, WHITELIST, +}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + Ok(match msg { + QueryMsg::Config {} => get_config(deps)?, + QueryMsg::Validators {} => get_validators(deps)?, + QueryMsg::WhitelistedAssets {} => get_whitelisted_assets(deps)?, + QueryMsg::RewardDistribution {} => get_rewards_distribution(deps)?, + QueryMsg::StakedBalance(asset_query) => get_staked_balance(deps, asset_query)?, + QueryMsg::PendingRewards(asset_query) => get_pending_rewards(deps, asset_query)?, + QueryMsg::AllStakedBalances(query) => get_all_staked_balances(deps, query)?, + QueryMsg::AllPendingRewards(query) => get_all_pending_rewards(deps, query)?, + QueryMsg::TotalStakedBalances {} => get_total_staked_balances(deps)?, + }) +} + +fn get_config(deps: Deps) -> StdResult { + let cfg = CONFIG.load(deps.storage)?; + + to_json_binary(&cfg) +} + +fn get_validators(deps: Deps) -> StdResult { + let validators = VALIDATORS.load(deps.storage)?; + + to_json_binary(&validators) +} + +fn get_whitelisted_assets(deps: Deps) -> StdResult { + let whitelist = WHITELIST.range(deps.storage, None, None, Order::Ascending); + let mut res: WhitelistedAssetsResponse = HashMap::new(); + + for item in whitelist { + let (key, chain_id) = item?; + let asset = key.check(deps.api, None)?; + res.entry(chain_id).or_insert_with(Vec::new).push(asset) + } + + to_json_binary(&res) +} + +fn get_rewards_distribution(deps: Deps) -> StdResult { + let asset_rewards_distr = ASSET_REWARD_DISTRIBUTION.load(deps.storage)?; + + to_json_binary(&asset_rewards_distr) +} + +fn get_staked_balance(deps: Deps, asset_query: AssetQuery) -> StdResult { + let addr = deps.api.addr_validate(&asset_query.address)?; + let key = (addr, asset_query.asset.clone().into()); + let balance = BALANCES.load(deps.storage, key)?; + + to_json_binary(&StakedBalanceRes { + asset: asset_query.asset, + balance, + }) +} + +fn get_pending_rewards(deps: Deps, asset_query: AssetQuery) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let addr = deps.api.addr_validate(&asset_query.address)?; + let key = (addr, AssetInfoKey::from(asset_query.asset.clone())); + let user_reward_rate = USER_ASSET_REWARD_RATE.load(deps.storage, key.clone())?; + let asset_reward_rate = + ASSET_REWARD_RATE.load(deps.storage, AssetInfoKey::from(asset_query.asset.clone()))?; + let user_balance = BALANCES.load(deps.storage, key.clone())?; + let unclaimed_rewards = UNCLAIMED_REWARDS + .load(deps.storage, key) + .unwrap_or(Uint128::zero()); + let pending_rewards = (asset_reward_rate - user_reward_rate) * user_balance; + + to_json_binary(&PendingRewardsRes { + rewards: unclaimed_rewards + pending_rewards, + staked_asset: asset_query.asset, + reward_asset: AssetInfo::Native(config.reward_denom), + }) +} + +fn get_all_staked_balances(deps: Deps, asset_query: AllStakedBalancesQuery) -> StdResult { + let addr = deps.api.addr_validate(&asset_query.address)?; + let whitelist = WHITELIST.range(deps.storage, None, None, Order::Ascending); + let mut res: Vec = Vec::new(); + + for asset_res in whitelist { + // Build the required key to recover the BALANCES + let (asset_key, _) = asset_res?; + let checked_asset_info = asset_key.check(deps.api, None)?; + let asset_info_key = AssetInfoKey::from(checked_asset_info.clone()); + let stake_key = (addr.clone(), asset_info_key); + let balance = BALANCES + .load(deps.storage, stake_key) + .unwrap_or(Uint128::zero()); + + // Append the request + res.push(StakedBalanceRes { + asset: checked_asset_info, + balance, + }) + } + + to_json_binary(&res) +} + +fn get_all_pending_rewards(deps: Deps, query: AllPendingRewardsQuery) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let addr = deps.api.addr_validate(&query.address)?; + let all_pending_rewards: StdResult> = USER_ASSET_REWARD_RATE + .prefix(addr.clone()) + .range(deps.storage, None, None, Order::Ascending) + .map(|item| { + let (asset, user_reward_rate) = item?; + let asset = asset.check(deps.api, None)?; + let asset_reward_rate = + ASSET_REWARD_RATE.load(deps.storage, AssetInfoKey::from(asset.clone()))?; + let user_balance = BALANCES.load( + deps.storage, + (addr.clone(), AssetInfoKey::from(asset.clone())), + )?; + let unclaimed_rewards = UNCLAIMED_REWARDS + .load( + deps.storage, + (addr.clone(), AssetInfoKey::from(asset.clone())), + ) + .unwrap_or(Uint128::zero()); + let pending_rewards = (asset_reward_rate - user_reward_rate) * user_balance; + Ok(PendingRewardsRes { + rewards: pending_rewards + unclaimed_rewards, + staked_asset: asset, + reward_asset: AssetInfo::Native(config.reward_denom.to_string()), + }) + }) + .collect::>>(); + + to_json_binary(&all_pending_rewards?) +} + +fn get_total_staked_balances(deps: Deps) -> StdResult { + let total_staked_balances: StdResult> = TOTAL_BALANCES + .range(deps.storage, None, None, Order::Ascending) + .map(|total_balance| -> StdResult { + let (asset, balance) = total_balance?; + Ok(StakedBalanceRes { + asset: asset.check(deps.api, None)?, + balance, + }) + }) + .collect(); + to_json_binary(&total_staked_balances?) +} diff --git a/contracts/alliance-lp-hub/src/state.rs b/contracts/alliance-lp-hub/src/state.rs new file mode 100644 index 0000000..65e0ccd --- /dev/null +++ b/contracts/alliance-lp-hub/src/state.rs @@ -0,0 +1,24 @@ +use alliance_protocol::alliance_oracle_types::ChainId; +use alliance_protocol::alliance_protocol::AssetDistribution; +use cosmwasm_std::{Addr, Decimal, Uint128}; +use cw_asset::AssetInfoKey; +use cw_storage_plus::{Item, Map}; +use std::collections::HashSet; +use crate::models::Config; + + +pub const CONFIG: Item = Item::new("config"); +pub const WHITELIST: Map = Map::new("whitelist"); + +pub const BALANCES: Map<(Addr, AssetInfoKey), Uint128> = Map::new("balances"); +pub const TOTAL_BALANCES: Map = Map::new("total_balances"); + +pub const VALIDATORS: Item> = Item::new("validators"); + +pub const ASSET_REWARD_DISTRIBUTION: Item> = + Item::new("asset_reward_distribution"); +pub const ASSET_REWARD_RATE: Map = Map::new("asset_reward_rate"); +pub const USER_ASSET_REWARD_RATE: Map<(Addr, AssetInfoKey), Decimal> = Map::new("user_asset_reward_rate"); +pub const UNCLAIMED_REWARDS: Map<(Addr, AssetInfoKey), Uint128> = Map::new("unclaimed_rewards"); + +pub const TEMP_BALANCE: Item = Item::new("temp_balance"); diff --git a/contracts/alliance-lp-hub/src/tests/helpers.rs b/contracts/alliance-lp-hub/src/tests/helpers.rs new file mode 100644 index 0000000..44002d9 --- /dev/null +++ b/contracts/alliance-lp-hub/src/tests/helpers.rs @@ -0,0 +1,17 @@ +use crate::contract::instantiate; +use crate::models::InstantiateMsg; +use alliance_protocol::token_factory::CustomExecuteMsg; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{DepsMut, Response}; + +pub fn setup_contract(deps: DepsMut) -> Response { + let info = mock_info("admin", &[]); + let env = mock_env(); + + let init_msg = InstantiateMsg { + governance: "gov".to_string(), + controller: "controller".to_string(), + reward_denom: "uluna".to_string(), + }; + instantiate(deps, env, info, init_msg).unwrap() +} diff --git a/contracts/alliance-lp-hub/src/tests/instantiate.rs b/contracts/alliance-lp-hub/src/tests/instantiate.rs new file mode 100644 index 0000000..eeeb115 --- /dev/null +++ b/contracts/alliance-lp-hub/src/tests/instantiate.rs @@ -0,0 +1,113 @@ +use crate::contract::reply; +use crate::query::query; +use crate::tests::helpers::setup_contract; +use alliance_protocol::token_factory::{CustomExecuteMsg, DenomUnit, Metadata, TokenExecuteMsg}; +use crate::models::{Config, QueryMsg}; +use cosmwasm_std::testing::{mock_dependencies, mock_env}; +use cosmwasm_std::{ + from_json, Addr, Binary, CosmosMsg, Reply, + Response, SubMsg, SubMsgResponse, SubMsgResult, + Uint128, +}; +use terra_proto_rs::traits::MessageExt; + +#[test] +fn test_setup_contract() { + let mut deps = mock_dependencies(); + let res = setup_contract(deps.as_mut()); + let denom = "ualliancelp"; + assert_eq!( + res, + Response::default() + .add_attributes(vec![("action", "instantiate"),]) + .add_submessage(SubMsg::reply_on_success( + CosmosMsg::Custom(CustomExecuteMsg::Token(TokenExecuteMsg::CreateDenom { + subdenom: denom.to_string(), + })), + 1 + )) + ); + + // Instantiate is a two steps process that's why + // alliance_token_denom and alliance_token_supply + // will be populated on reply. + let query_config = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + let config: Config = from_json(query_config).unwrap(); + assert_eq!( + config, + Config { + governance: Addr::unchecked("gov"), + controller: Addr::unchecked("controller"), + reward_denom: "uluna".to_string(), + alliance_token_denom: "".to_string(), + alliance_token_supply: Uint128::new(0), + } + ); +} + +#[test] +fn test_reply_create_token() { + let mut deps = mock_dependencies(); + setup_contract(deps.as_mut()); + + // Build reply message + let msg = Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(Binary::from( + String::from("factory/cosmos2contract/ualliancelp") + .to_bytes() + .unwrap(), + )), + }), + }; + let res = reply(deps.as_mut(), mock_env(), msg).unwrap(); + let sub_msg = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( + TokenExecuteMsg::MintTokens { + amount: Uint128::from(1000000000000u128), + denom: "factory/cosmos2contract/ualliancelp".to_string(), + mint_to_address: "cosmos2contract".to_string(), + }, + ))); + let sub_msg_metadata = SubMsg::new(CosmosMsg::Custom(CustomExecuteMsg::Token( + TokenExecuteMsg::SetMetadata { + denom: "factory/cosmos2contract/ualliancelp".to_string(), + metadata: Metadata { + description: "Staking token for the alliance protocol lp contract".to_string(), + denom_units: vec![DenomUnit { + denom: "factory/cosmos2contract/ualliancelp".to_string(), + exponent: 0, + aliases: vec![], + }], + base: "factory/cosmos2contract/ualliancelp".to_string(), + display: "factory/cosmos2contract/ualliancelp".to_string(), + name: "Alliance LP Token".to_string(), + symbol: "ALLIANCE_LP".to_string(), + }, + }, + ))); + assert_eq!( + res, + Response::default() + .add_attributes(vec![ + ("alliance_token_denom", "factory/cosmos2contract/ualliancelp"), + ("alliance_token_total_supply", "1000000000000"), + ]) + .add_submessage(sub_msg) + .add_submessage(sub_msg_metadata) + ); + + let query_config = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + let config: Config = from_json(query_config).unwrap(); + assert_eq!( + config, + Config { + governance: Addr::unchecked("gov"), + controller: Addr::unchecked("controller"), + reward_denom: "uluna".to_string(), + alliance_token_denom: "factory/cosmos2contract/ualliancelp".to_string(), + alliance_token_supply: Uint128::new(1000000000000), + } + ); +} diff --git a/contracts/alliance-lp-hub/src/tests/mod.rs b/contracts/alliance-lp-hub/src/tests/mod.rs new file mode 100644 index 0000000..0d4cca2 --- /dev/null +++ b/contracts/alliance-lp-hub/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod instantiate; +mod helpers; \ No newline at end of file diff --git a/contracts/alliance-oracle/src/contract.rs b/contracts/alliance-oracle/src/contract.rs index 04c57a8..adb71a0 100644 --- a/contracts/alliance-oracle/src/contract.rs +++ b/contracts/alliance-oracle/src/contract.rs @@ -1,11 +1,14 @@ use std::collections::HashMap; use std::env; -use alliance_protocol::alliance_oracle_types::{ - AssetStaked, ChainId, ChainInfo, ChainsInfo, Config, EmissionsDistribution, ExecuteMsg, Expire, - InstantiateMsg, MigrateMsg, QueryMsg, +use alliance_protocol::{ + alliance_oracle_types::{ + AssetStaked, ChainId, ChainInfo, ChainsInfo, Config, EmissionsDistribution, ExecuteMsg, Expire, + InstantiateMsg, MigrateMsg, QueryMsg, + }, + signed_decimal::{Sign, SignedDecimal}, + error::ContractError, }; -use alliance_protocol::signed_decimal::{Sign, SignedDecimal}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -13,7 +16,6 @@ use cosmwasm_std::{ }; use cw2::set_contract_version; -use crate::error::ContractError; use crate::state::{CHAINS_INFO, CONFIG, LUNA_INFO}; use crate::utils; diff --git a/contracts/alliance-oracle/src/error.rs b/contracts/alliance-oracle/src/error.rs deleted file mode 100644 index 9a5e449..0000000 --- a/contracts/alliance-oracle/src/error.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_std::StdError; -use thiserror::Error; - -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("Unauthorized")] - Unauthorized {}, -} diff --git a/contracts/alliance-oracle/src/lib.rs b/contracts/alliance-oracle/src/lib.rs index 5c54282..686c457 100644 --- a/contracts/alliance-oracle/src/lib.rs +++ b/contracts/alliance-oracle/src/lib.rs @@ -1,5 +1,4 @@ pub mod contract; -mod error; pub mod state; #[cfg(test)] pub mod tests; diff --git a/contracts/alliance-oracle/src/utils.rs b/contracts/alliance-oracle/src/utils.rs index 11fb137..8020907 100644 --- a/contracts/alliance-oracle/src/utils.rs +++ b/contracts/alliance-oracle/src/utils.rs @@ -1,7 +1,6 @@ use alliance_protocol::alliance_oracle_types::Config; use cosmwasm_std::Addr; - -use crate::error::ContractError; +use alliance_protocol::error::ContractError; pub fn authorize_execution(config: Config, addr: Addr) -> Result<(), ContractError> { if addr != config.controller_addr { diff --git a/packages/alliance-protocol/src/alliance_protocol.rs b/packages/alliance-protocol/src/alliance_protocol.rs index fefb6df..7fa5536 100644 --- a/packages/alliance-protocol/src/alliance_protocol.rs +++ b/packages/alliance-protocol/src/alliance_protocol.rs @@ -1,51 +1,6 @@ -use crate::alliance_oracle_types::ChainId; -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Timestamp, Uint128}; -use cw_asset::{Asset, AssetInfo}; -use std::collections::{HashMap, HashSet}; -#[cw_serde] -pub struct Config { - pub governance: Addr, - pub controller: Addr, - pub oracle: Addr, - pub last_reward_update_timestamp: Timestamp, - pub alliance_token_denom: String, - pub alliance_token_supply: Uint128, - pub reward_denom: String, -} - -#[cw_serde] -pub struct AssetDistribution { - pub asset: AssetInfo, - pub distribution: Decimal, -} - -#[cw_serde] -pub struct InstantiateMsg { - pub governance: String, - pub controller: String, - pub oracle: String, - pub reward_denom: String, -} - -#[cw_serde] -pub enum ExecuteMsg { - // Public functions - Stake {}, - Unstake(Asset), - ClaimRewards(AssetInfo), - UpdateRewards {}, - - // Privileged functions - WhitelistAssets(HashMap>), - RemoveAssets(Vec), - UpdateRewardsCallback {}, - AllianceDelegate(AllianceDelegateMsg), - AllianceUndelegate(AllianceUndelegateMsg), - AllianceRedelegate(AllianceRedelegateMsg), - RebalanceEmissions {}, - RebalanceEmissionsCallback {}, -} +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Uint128, Decimal}; +use cw_asset::AssetInfo; #[cw_serde] pub struct AllianceDelegation { @@ -75,67 +30,12 @@ pub struct AllianceRedelegateMsg { pub redelegations: Vec, } -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(Config)] - Config {}, - - #[returns(HashSet)] - Validators {}, - - #[returns(WhitelistedAssetsResponse)] - WhitelistedAssets {}, - - #[returns(Vec)] - RewardDistribution {}, - - #[returns(StakedBalanceRes)] - StakedBalance(AssetQuery), - - #[returns(PendingRewardsRes)] - PendingRewards(AssetQuery), - - #[returns(Vec)] - AllStakedBalances(AllStakedBalancesQuery), - - #[returns(Vec)] - AllPendingRewards(AllPendingRewardsQuery), - - #[returns(Vec)] - TotalStakedBalances {}, -} - -pub type WhitelistedAssetsResponse = HashMap>; - -#[cw_serde] -pub struct AssetQuery { - pub address: String, - pub asset: AssetInfo, -} - -#[cw_serde] -pub struct AllStakedBalancesQuery { - pub address: String, -} - -#[cw_serde] -pub struct AllPendingRewardsQuery { - pub address: String, -} - #[cw_serde] pub struct MigrateMsg {} -#[cw_serde] -pub struct StakedBalanceRes { - pub asset: AssetInfo, - pub balance: Uint128, -} #[cw_serde] -pub struct PendingRewardsRes { - pub staked_asset: AssetInfo, - pub reward_asset: AssetInfo, - pub rewards: Uint128, +pub struct AssetDistribution { + pub asset: AssetInfo, + pub distribution: Decimal, } diff --git a/contracts/alliance-hub/src/error.rs b/packages/alliance-protocol/src/error.rs similarity index 100% rename from contracts/alliance-hub/src/error.rs rename to packages/alliance-protocol/src/error.rs diff --git a/packages/alliance-protocol/src/lib.rs b/packages/alliance-protocol/src/lib.rs index 8423513..fee2d51 100644 --- a/packages/alliance-protocol/src/lib.rs +++ b/packages/alliance-protocol/src/lib.rs @@ -1,3 +1,5 @@ pub mod alliance_oracle_types; pub mod alliance_protocol; pub mod signed_decimal; +pub mod token_factory; +pub mod error; \ No newline at end of file diff --git a/contracts/alliance-hub/src/token_factory.rs b/packages/alliance-protocol/src/token_factory.rs similarity index 100% rename from contracts/alliance-hub/src/token_factory.rs rename to packages/alliance-protocol/src/token_factory.rs