diff --git a/.gitmodules b/.gitmodules index 50ce54ea8..14cc8eabc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,4 +5,4 @@ [submodule "contracts/shd_staking"] path = contracts/shd_staking url = https://github.com/securesecrets/SPIP-STKN-0 - branch = staking-implementation \ No newline at end of file + branch = staking-implementation diff --git a/Cargo.toml b/Cargo.toml index e6f5a83c1..5e0c335ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,18 @@ members = [ "contracts/governance", "contracts/mint", "contracts/mint_router", - "contracts/treasury", "contracts/oracle", "contracts/snip20", - "contracts/shd_staking", + + # DAO + # - Core + "contracts/treasury", + "contracts/treasury_manager", + # - Adapters "contracts/scrt_staking", + "contracts/rewards_emission", + + "contracts/shd_staking", # Mock contracts "contracts/mock_band", diff --git a/contractlib/secretlib/secretlib.py b/contractlib/secretlib/secretlib.py index 872300522..858de9a76 100644 --- a/contractlib/secretlib/secretlib.py +++ b/contractlib/secretlib/secretlib.py @@ -123,7 +123,8 @@ def run_command_compute_hash(command): try: txhash = json.loads(out)["txhash"] - # print(txhash) + #print(txhash) + except Exception as e: # print(out) raise e diff --git a/contracts/mint/Cargo.toml b/contracts/mint/Cargo.toml index 5510c722e..4c2540ad5 100644 --- a/contracts/mint/Cargo.toml +++ b/contracts/mint/Cargo.toml @@ -31,15 +31,14 @@ cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } cosmwasm-schema = "0.10.1" secret-toolkit = { version = "0.2" } cosmwasm-math-compat = { path = "../../packages/cosmwasm_math_compat" } -shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol", features = [ - "mint", - "oracle", - "band", - "dex", -] } +shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol", features = [ "mint", "oracle", "band", "dex", ] } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } snafu = { version = "0.6.3" } -mockall = "0.10.2" -mockall_double = "0.2.0" chrono = "0.4.19" + +[dev-dependencies] +fadroma = { branch = "v100", git = "https://github.com/hackbg/fadroma.git" } +snip20-reference-impl = { version = "0.1.0", path = "../../contracts/snip20" } +oracle = { version = "0.1.0", path = "../../contracts/oracle" } +mock_band = { version = "0.1.0", path = "../../contracts/mock_band" } diff --git a/contracts/mint/src/handle.rs b/contracts/mint/src/handle.rs index 9d46932f6..3ac78192a 100644 --- a/contracts/mint/src/handle.rs +++ b/contracts/mint/src/handle.rs @@ -484,6 +484,24 @@ pub fn calculate_mint( } } +/* +pub fn calculate_fee_curve( + // "Centered" + base_fee: Uint128, + // How far off from where we want (abs(desired_price - cur_price)) + price_skew: Uint128, + // skew we should never reach (where fee maxes out) + asymptote: Uint128, +) -> Uint128 { + + /* aggressiveness is how sharply it turns up at the asymptote + * speed is the overall speed of increase + * how to include asymptote to push the threshold before acceleration? + * y = (x + speed) ^ (2 * aggressiveness) + */ +} +*/ + pub fn calculate_portion(amount: Uint128, portion: Uint128) -> Uint128 { /* amount: total amount sent to burn (uSSCRT/uSILK/uSHD) * portion: percent * 10^18 e.g. 5_320_000_000_000_000_000 = 5.32% = .0532 @@ -507,5 +525,6 @@ fn oracle( config.oracle.code_hash, config.oracle.address, )?; - Ok(answer.rate) + + Ok(Uint128::from(answer.rate)) } diff --git a/contracts/mint/src/lib.rs b/contracts/mint/src/lib.rs index 5ed186c7b..6eb252d2b 100644 --- a/contracts/mint/src/lib.rs +++ b/contracts/mint/src/lib.rs @@ -3,9 +3,6 @@ pub mod handle; pub mod query; pub mod state; -#[cfg(test)] -mod test; - #[cfg(target_arch = "wasm32")] mod wasm { use super::contract; diff --git a/contracts/mint/src/query.rs b/contracts/mint/src/query.rs index f1ea7a1aa..7206fdd53 100644 --- a/contracts/mint/src/query.rs +++ b/contracts/mint/src/query.rs @@ -64,12 +64,14 @@ pub fn mint( offer_asset: HumanAddr, amount: Uint128, ) -> StdResult { + let native_asset = native_asset_r(&deps.storage).load()?; match assets_r(&deps.storage).may_load(offer_asset.to_string().as_bytes())? { Some(asset) => { - let fee = calculate_portion(amount, asset.fee); - let amount = mint_amount(deps, amount.checked_sub(fee)?, &asset, &native_asset)?; + //let fee = calculate_portion(amount, asset.fee); + //let amount = mint_amount(deps, amount.checked_sub(fee)?, &asset, &native_asset)?; + let amount = mint_amount(deps, amount, &asset, &native_asset)?; Ok(QueryAnswer::Mint { asset: native_asset.contract, amount, diff --git a/contracts/mint/src/test.rs b/contracts/mint/src/test.rs deleted file mode 100644 index b8ef1430f..000000000 --- a/contracts/mint/src/test.rs +++ /dev/null @@ -1,482 +0,0 @@ -#[cfg(test)] -pub mod tests { - use cosmwasm_math_compat::Uint128; - use cosmwasm_std::{ - coins, from_binary, - testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}, - Extern, HumanAddr, StdError, - }; - use mockall_double::double; - use shade_protocol::{ - mint::{HandleMsg, InitMsg, QueryAnswer, QueryMsg}, - utils::price::{normalize_price, translate_price}, - }; - - use crate::{ - contract::{handle, init, query}, - handle::{calculate_mint, calculate_portion, try_burn}, - }; - - mod mock_secret_toolkit { - - use cosmwasm_math_compat::Uint128; - use cosmwasm_std::{HumanAddr, Querier, StdResult}; - use secret_toolkit::snip20::TokenInfo; - - pub fn mock_token_info_query( - _querier: &Q, - _block_size: usize, - _callback_code_hash: String, - _contract_addr: HumanAddr, - ) -> StdResult { - Ok(TokenInfo { - name: "Token".to_string(), - symbol: "TKN".to_string(), - decimals: 6, - total_supply: Some(Uint128::new(150u128).into()), - }) - } - } - - #[double] - use mock_secret_toolkit::token_info_query; - use shade_protocol::utils::asset::Contract; - - fn create_contract(address: &str, code_hash: &str) -> Contract { - let env = mock_env(address.to_string(), &[]); - return Contract { - address: env.message.sender, - code_hash: code_hash.to_string(), - }; - } - - fn dummy_init( - admin: String, - native_asset: Contract, - oracle: Contract, - peg: Option, - treasury: HumanAddr, - capture: Option, - ) -> Extern { - let mut deps = mock_dependencies(20, &[]); - let msg = InitMsg { - admin: None, - native_asset, - oracle, - peg, - treasury, - secondary_burn: None, - limit: None, - }; - let env = mock_env(admin, &coins(1000, "earth")); - let _res = init(&mut deps, env, msg).unwrap(); - - return deps; - } - - #[test] - /* - fn proper_initialization() { - let mut deps = mock_dependencies(20, &[]); - let msg = InitMsg { - admin: None, - native_asset: create_contract("", ""), - oracle: create_contract("", ""), - peg: Option::from("TKN".to_string()), - treasury: Option::from(create_contract("", "")), - // 1% - capture: Option::from(Uint128(100)), - }; - let env = mock_env("creator", &coins(1000, "earth")); - - // we can just call .unwrap() to assert this was a success - let res = init(&mut deps, env, msg).unwrap(); - assert_eq!(0, res.messages.len()); - } - */ - - /* - #[test] - fn config_update() { - let native_asset = create_contract("snip20", "hash"); - let oracle = create_contract("oracle", "hash"); - let treasury = create_contract("treasury", "hash"); - let capture = Uint128(100); - - let admin_env = mock_env("admin", &coins(1000, "earth")); - let mut deps = dummy_init("admin".to_string(), - native_asset, - oracle, - None, - Option::from(treasury), - Option::from(capture)); - - // new config vars - let new_oracle = Option::from(create_contract("new_oracle", "hash")); - let new_treasury = Option::from(create_contract("new_treasury", "hash")); - let new_capture = Option::from(Uint128(200)); - - // Update config - let update_msg = HandleMsg::UpdateConfig { - owner: None, - oracle: new_oracle.clone(), - treasury: new_treasury.clone(), - // 2% - capture: new_capture.clone(), - }; - let update_res = handle(&mut deps, admin_env, update_msg); - - let config_res = query(&deps, QueryMsg::GetConfig {}).unwrap(); - let value: QueryAnswer = from_binary(&config_res).unwrap(); - match value { - QueryAnswer::Config { config } => { - assert_eq!(config.oracle, new_oracle.unwrap()); - assert_eq!(config.treasury, new_treasury); - assert_eq!(config.capture, new_capture); - } - _ => { panic!("Received wrong answer") } - } - } - */ - - /* - #[test] - fn user_register_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", ""), - create_contract("", ""), - None, None, None); - - // User should not be allowed to add an item - let user_env = mock_env("user", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let res = handle(&mut deps, user_env, msg); - match res { - Err(StdError::Unauthorized { .. }) => {} - _ => panic!("Must return unauthorized error"), - } - - // Response should be an empty array - let res = query(&deps, QueryMsg::GetSupportedAssets {}).unwrap(); - let value: QueryAnswer = from_binary(&res).unwrap(); - match value { - QueryAnswer::SupportedAssets { assets } => { assert_eq!(0, assets.len()) } - _ => { panic!("Received wrong answer") } - } - } - - #[test] - fn admin_register_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", ""), - create_contract("", ""), - None, - None, - None); - - // Admin should be allowed to add an item - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let _res = handle(&mut deps, env, msg).unwrap(); - - // Response should be an array of size 1 - let res = query(&deps, QueryMsg::GetSupportedAssets {}).unwrap(); - let value: QueryAnswer = from_binary(&res).unwrap(); - match value { - QueryAnswer::SupportedAssets { assets } => { assert_eq!(1, assets.len()) } - _ => { panic!("Received wrong answer") } - } - } - - #[test] - fn duplicate_register_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", ""), - create_contract("", ""), - None, - None, - None); - - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let _res = handle(&mut deps, env, msg).unwrap(); - - // Should not be allowed to add an existing asset - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "other_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let res = handle(&mut deps, env, msg); - match res { - Err(StdError::GenericErr { .. }) => {} - _ => panic!("Must return not found error"), - }; - } - - /* - #[test] - fn user_update_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", ""), - create_contract("", "")); - - // Add a supported asset - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let _res = handle(&mut deps, env, msg).unwrap(); - - // users should not be allowed to update assets - let user_env = mock_env("user", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let new_dummy_contract = create_contract("some_other_contract", "some_hash"); - let msg = HandleMsg::UpdateAsset { - asset: dummy_contract.address, - contract: new_dummy_contract, - }; - let res = handle(&mut deps, user_env, msg); - match res { - Err(StdError::Unauthorized { .. }) => {} - _ => panic!("Must return unauthorized error"), - }; - } - */ - - /* - #[test] - fn admin_update_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", ""), - create_contract("", "")); - - // Add a supported asset - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let _res = handle(&mut deps, env, msg).unwrap(); - - // admins can update assets - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let new_dummy_contract = create_contract("some_other_contract", "some_hash"); - let msg = HandleMsg::UpdateAsset { - asset: dummy_contract.address, - contract: new_dummy_contract, - }; - let _res = handle(&mut deps, env, msg).unwrap(); - - // Response should be new dummy contract - let res = query(&deps, QueryMsg::GetAsset { contract: "some_other_contract".to_string() }).unwrap(); - let value: QueryAnswer = from_binary(&res).unwrap(); - match value { - QueryAnswer::Asset { asset, burned } => { assert_eq!("some_other_contract".to_string(), asset.contract.address.to_string()) } - _ => { panic!("Received wrong answer") } - }; - } - */ - - #[test] - fn receiving_an_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", ""), - create_contract("", ""), - None, None, None); - - // Add a supported asset - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let _res = handle(&mut deps, env, msg).unwrap(); - - // Contract tries to send funds - let env = mock_env("some_contract", &coins(1000, "earth")); - let dummy_contract = create_contract("some_owner", "some_hash"); - - let msg = HandleMsg::Receive { - sender: dummy_contract.address, - from: Default::default(), - amount: Uint128(100), - msg: None, - memo: None - }; - - let res = handle(&mut deps, env, msg); - match res { - Err(err) => { - match err { - StdError::NotFound { .. } => {panic!("Not found");} - StdError::Unauthorized { .. } => {panic!("Unauthorized");} - _ => {} - } - } - _ => {} - } - } - - #[test] - fn receiving_an_asset_from_non_supported_asset() { - let mut deps = dummy_init("admin".to_string(), - create_contract("", ""), - create_contract("", ""), - None, - None, - None, - ); - - // Add a supported asset - let env = mock_env("admin", &coins(1000, "earth")); - let dummy_contract = create_contract("some_contract", "some_hash"); - let msg = HandleMsg::RegisterAsset { - contract: dummy_contract, - }; - let _res = handle(&mut deps, env, msg).unwrap(); - - // Contract tries to send funds - let env = mock_env("some_other_contract", &coins(1000, "earth")); - let dummy_contract = create_contract("some_owner", "some_hash"); - let msg = HandleMsg::Receive { - sender: dummy_contract.address, - from: Default::default(), - amount: Uint128(100), - msg: None, - memo: None - }; - let res = handle(&mut deps, env, msg); - match res { - Err(StdError::NotFound { .. }) => {} - _ => {panic!("Must return not found error")}, - } - } - */ - #[test] - fn capture_calc() { - let amount = Uint128::new(1_000_000_000_000_000_000u128); - //10% - let capture = Uint128::new(100_000_000_000_000_000u128); - let expected = Uint128::new(100_000_000_000_000_000u128); - let value = calculate_portion(amount, capture); - assert_eq!(value, expected); - } - - /** - #[test] - fn mint_algorithm_simple() { - // In this example the "sent" value is 1 with 6 decimal places - // The mint value will be 1 with 3 decimal places - let price = Uint128::new(1_000_000_000_000_000_000u128); - let in_amount = Uint128::new(1_000_000u128); - let expected_value = Uint128::new(1_000u128); - let value = calculate_mint(price, in_amount, 6, price, 3); - - assert_eq!(value, expected_value); - } - - #[test] - fn mint_algorithm_complex_1() { - // In this example the "sent" value is 1.8 with 6 decimal places - // The mint value will be 3.6 with 12 decimal places - let in_price = Uint128::new(2_000_000_000_000_000_000u128); - let target_price = Uint128::new(1_000_000_000_000_000_000u128); - let in_amount = Uint128::new(1_800_000u128); - let expected_value = Uint128::new(3_600_000_000_000u128); - let value = calculate_mint(in_price, in_amount, 6, target_price, 12); - - assert_eq!(value, expected_value); - } - - #[test] - fn mint_algorithm_complex_2() { - // In amount is 50.000 valued at 20 - // target price is 100$ with 6 decimals - let in_price = Uint128::new(20_000_000_000_000_000_000u128); - let target_price = Uint128::new(100_000_000_000_000_000_000u128); - let in_amount = Uint128::new(50_000u128); - let expected_value = Uint128::new(10_000_000u128); - let value = calculate_mint(in_price, in_amount, 3, target_price, 6); - - assert_eq!(value, expected_value); - } - **/ - macro_rules! mint_algorithm_tests { - ($($name:ident: $value:expr,)*) => { - $( - #[test] - fn $name() { - let (in_price, in_amount, in_decimals, target_price, target_decimals, expected_value) = $value; - assert_eq!(calculate_mint(in_price, in_amount, in_decimals, target_price, target_decimals), expected_value); - } - )* - } - } - - mint_algorithm_tests! { - mint_simple_0: ( - // In this example the "sent" value is 1 with 6 decimal places - // The mint value will be 1 with 3 decimal places - Uint128::new(1_000_000_000_000_000_000), //Burn price - Uint128::new(1_000_000), //Burn amount - 6u8, //Burn decimals - Uint128::new(1_000_000_000_000_000_000), //Mint price - 3u8, //Mint decimals - Uint128::new(1_000), //Expected value - ), - mint_complex_0: ( - // In this example the "sent" value is 1.8 with 6 decimal places - // The mint value will be 3.6 with 12 decimal places - Uint128::new(2_000_000_000_000_000_000), - Uint128::new(1_800_000), - 6u8, - Uint128::new(1_000_000_000_000_000_000), - 12u8, - Uint128::new(3_600_000_000_000), - ), - mint_complex_1: ( - // In amount is 50.000 valued at 20 - // target price is 100$ with 6 decimals - Uint128::new(20_000_000_000_000_000_000), - Uint128::new(50_000), - 3u8, - Uint128::new(100_000_000_000_000_000_000), - 6u8, - Uint128::new(10_000_000), - ), - mint_complex_2: ( - // In amount is 10,000,000 valued at 100 - // Target price is $10 with 6 decimals - Uint128::new(1_000_000_000_000_000_000_000), - Uint128::new(100_000_000_000_000), - 8u8, - Uint128::new(10_000_000_000_000_000_000), - 6u8, - Uint128::new(100_000_000_000_000), - ), - } - /* - mint_overflow_0: ( - // In amount is 1,000,000,000,000,000,000,000,000 valued at 1,000 - // Target price is $5 with 6 decimals - Uint128(1_000_000_000_000_000_000_000), - Uint128(100_000_000_000_000_000_000_000_000_000_000), - 8u8, - Uint128(5_000_000_000_000_000_000), - 6u8, - Uint128(500_000_000_000_000_000_000_000_000_000_000_000), - ), - */ -} diff --git a/contracts/mint/tests/integration.rs b/contracts/mint/tests/integration.rs new file mode 100644 index 000000000..9ae431e82 --- /dev/null +++ b/contracts/mint/tests/integration.rs @@ -0,0 +1,368 @@ +use cosmwasm_math_compat as compat; +use cosmwasm_std::{ + coins, from_binary, to_binary, + Extern, HumanAddr, StdError, + Binary, StdResult, HandleResponse, Env, + InitResponse, Uint128, +}; + +use shade_protocol::{ + mint::{HandleMsg, InitMsg, QueryAnswer, QueryMsg}, + utils::{ + asset::Contract, + price::{normalize_price, translate_price}, + }, + band::{ ReferenceData, BandQuery }, +}; + +use snip20_reference_impl; +use oracle; +use mock_band; + +use mint::{ + contract::{handle, init, query}, + handle::{calculate_mint, calculate_portion, try_burn}, +}; + +use fadroma::{ + ContractLink, + ensemble::{ + MockEnv, MockDeps, + ContractHarness, ContractEnsemble, + }, +}; + +pub struct Mint; + +impl ContractHarness for Mint { + // Use the method from the default implementation + fn init(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + init( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + fn handle(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + handle( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + // Override with some hardcoded value for the ease of testing + fn query(&self, deps: &MockDeps, msg: Binary) -> StdResult { + query( + deps, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } +} + +pub struct MockBand; + +impl ContractHarness for MockBand { + // Use the method from the default implementation + fn init(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + mock_band::contract::init( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + fn handle(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + mock_band::contract::handle( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + // Override with some hardcoded value for the ease of testing + fn query(&self, deps: &MockDeps, msg: Binary) -> StdResult { + mock_band::contract::query( + deps, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } +} + +pub struct Snip20; + +impl ContractHarness for Snip20 { + // Use the method from the default implementation + fn init(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + snip20_reference_impl::contract::init( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + fn handle(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + snip20_reference_impl::contract::handle( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + // Override with some hardcoded value for the ease of testing + fn query(&self, deps: &MockDeps, msg: Binary) -> StdResult { + snip20_reference_impl::contract::query( + deps, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } +} + +pub struct Oracle; + +impl ContractHarness for Oracle { + // Use the method from the default implementation + fn init(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + oracle::contract::init( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + fn handle(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + oracle::contract::handle( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + // Override with some hardcoded value for the ease of testing + fn query(&self, deps: &MockDeps, msg: Binary) -> StdResult { + oracle::contract::query( + deps, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } +} + +fn test_ensemble(offer_price: Uint128, offer_amount: Uint128, mint_price: Uint128, expected_amount: Uint128) { + + let mut ensemble = ContractEnsemble::new(50); + + let reg_oracle = ensemble.register(Box::new(Oracle)); + let reg_mint = ensemble.register(Box::new(Mint)); + let reg_snip20 = ensemble.register(Box::new(Snip20)); + let reg_band = ensemble.register(Box::new(MockBand)); + + let sscrt = ensemble.instantiate( + reg_snip20.id, + &snip20_reference_impl::msg::InitMsg { + name: "secretSCRT".into(), + admin: Some("admin".into()), + symbol: "SSCRT".into(), + decimals: 6, + initial_balances: None, + prng_seed: to_binary("").ok().unwrap(), + config: None, + }, + MockEnv::new( + "admin", + ContractLink { + address: HumanAddr("sscrt".into()), + code_hash: reg_snip20.code_hash.clone(), + } + ) + ).unwrap(); + + let shade = ensemble.instantiate( + reg_snip20.id, + &snip20_reference_impl::msg::InitMsg { + name: "Shade".into(), + admin: Some("admin".into()), + symbol: "SHD".into(), + decimals: 8, + initial_balances: None, + prng_seed: to_binary("").ok().unwrap(), + config: None, + }, + MockEnv::new( + "admin", + ContractLink { + address: HumanAddr("shade".into()), + code_hash: reg_snip20.code_hash.clone(), + } + ) + ).unwrap(); + + let band = ensemble.instantiate( + reg_band.id, + &shade_protocol::band::InitMsg { }, + MockEnv::new( + "admin", + ContractLink { + address: HumanAddr("band".into()), + code_hash: reg_band.code_hash.clone(), + } + ) + ).unwrap(); + + let oracle = ensemble.instantiate( + reg_oracle.id, + &shade_protocol::oracle::InitMsg { + admin: Some(HumanAddr("admin".into())), + band: Contract { + address: band.address.clone(), + code_hash: band.code_hash.clone(), + }, + sscrt: Contract { + address: sscrt.address.clone(), + code_hash: sscrt.code_hash.clone(), + }, + }, + MockEnv::new( + "admin", + ContractLink { + address: HumanAddr("oracle".into()), + code_hash: reg_oracle.code_hash.clone(), + } + ) + ).unwrap(); + + let mint = ensemble.instantiate( + reg_mint.id, + &shade_protocol::mint::InitMsg { + admin: Some(HumanAddr("admin".into())), + oracle: Contract { + address: oracle.address.clone(), + code_hash: oracle.code_hash.clone(), + }, + native_asset: Contract { + address: shade.address.clone(), + code_hash: shade.code_hash.clone(), + }, + peg: None, + treasury: HumanAddr("admin".into()), + secondary_burn: None, + limit: None, + }, + MockEnv::new( + "admin", + ContractLink { + address: HumanAddr("mint".into()), + code_hash: reg_mint.code_hash, + } + ) + ).unwrap(); + + // Setup price feeds + ensemble.execute( + &mock_band::contract::HandleMsg::MockPrice { + symbol: "SCRT".into(), + price: offer_price, + }, + MockEnv::new( + "admin", + band.clone(), + ), + ).unwrap(); + ensemble.execute( + &mock_band::contract::HandleMsg::MockPrice { + symbol: "SHD".into(), + price: mint_price, + }, + MockEnv::new( + "admin", + band.clone(), + ), + ).unwrap(); + + // Register sSCRT burn + ensemble.execute( + &shade_protocol::mint::HandleMsg::RegisterAsset { + contract: Contract { + address: sscrt.address.clone(), + code_hash: sscrt.code_hash.clone(), + }, + capture: None, + fee: None, + unlimited: None, + }, + MockEnv::new( + "admin", + mint.clone(), + ), + ).unwrap(); + + // Check mint query + let (asset, amount) = match ensemble.query( + mint.address.clone(), + &shade_protocol::mint::QueryMsg::Mint { + offer_asset: sscrt.address.clone(), + amount: compat::Uint128::new(offer_amount.u128()), + } + ).unwrap() { + shade_protocol::mint::QueryAnswer::Mint { asset, amount } => (asset, amount), + _ => (Contract { address: HumanAddr("".into()), code_hash: "".into()} , compat::Uint128::new(0)), + + }; + + assert_eq!(asset, Contract { + address: shade.address.clone(), + code_hash: shade.code_hash.clone(), + }); + + assert_eq!(amount, compat::Uint128::new(expected_amount.u128())); +} + +macro_rules! mint_int_tests { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (offer_price, offer_amount, mint_price, expected_amount) = $value; + test_ensemble(offer_price, offer_amount, mint_price, expected_amount); + } + )* + } +} +mint_int_tests! { + mint_int_0: ( + Uint128(10u128.pow(18)), // $1 + Uint128(10u128.pow(6)), // 1sscrt + Uint128(10u128.pow(18)), // $1 + Uint128(10u128.pow(8)), // 1 SHD + ), + mint_int_1: ( + Uint128(2 * 10u128.pow(18)), // $2 + Uint128(10u128.pow(6)), // 1 sscrt + Uint128(10u128.pow(18)), // $1 + Uint128(2 * 10u128.pow(8)), // 2 SHD + ), + mint_int_2: ( + Uint128(1 * 10u128.pow(18)), // $1 + Uint128(4 * 10u128.pow(6)), // 4 sscrt + Uint128(10u128.pow(18)), // $1 + Uint128(4 * 10u128.pow(8)), // 4 SHD + ), + mint_int_3: ( + Uint128(10 * 10u128.pow(18)), // $10 + Uint128(30 * 10u128.pow(6)), // 30 sscrt + Uint128(5 * 10u128.pow(18)), // $5 + Uint128(60 * 10u128.pow(8)), // 60 SHD + ), +} diff --git a/contracts/mint/tests/unit.rs b/contracts/mint/tests/unit.rs new file mode 100644 index 000000000..9856603c8 --- /dev/null +++ b/contracts/mint/tests/unit.rs @@ -0,0 +1,104 @@ +use cosmwasm_math_compat::Uint128; +use cosmwasm_std; +use cosmwasm_std::{ + coins, from_binary, to_binary, + Extern, HumanAddr, StdError, + Binary, StdResult, HandleResponse, Env, + InitResponse, +}; + +use shade_protocol::{ + mint::{HandleMsg, InitMsg, QueryAnswer, QueryMsg}, + utils::{ + asset::Contract, + price::{normalize_price, translate_price}, + }, + band::{ ReferenceData, BandQuery }, +}; + +#[test] +fn capture_calc() { + let amount = Uint128::new(1_000_000_000_000_000_000u128); + //10% + let capture = Uint128::new(100_000_000_000_000_000u128); + let expected = Uint128::new(100_000_000_000_000_000u128); + let value = mint::handle::calculate_portion(amount, capture); + assert_eq!(value, expected); +} + +macro_rules! mint_algorithm_tests { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (in_price, in_amount, in_decimals, target_price, target_decimals, expected_value) = $value; + assert_eq!(mint::handle::calculate_mint(in_price, in_amount, in_decimals, target_price, target_decimals), expected_value); + } + )* + } +} + +mint_algorithm_tests! { + mint_simple_0: ( + // In this example the "sent" value is 1 with 6 decimal places + // The mint value will be 1 with 3 decimal places + Uint128::new(1_000_000_000_000_000_000), //Burn price + Uint128::new(1_000_000), //Burn amount + 6u8, //Burn decimals + Uint128::new(1_000_000_000_000_000_000), //Mint price + 3u8, //Mint decimals + Uint128::new(1_000), //Expected value + ), + mint_simple_1: ( + // In this example the "sent" value is 1 with 8 decimal places + // The mint value will be 1 with 3 decimal places + Uint128::new(1_000_000_000_000_000_000), //Burn price + Uint128::new(1_000_000), //Burn amount + 6u8, //Burn decimals + Uint128::new(1_000_000_000_000_000_000), //Mint price + 8u8, //Mint decimals + Uint128::new(100_000_000), //Expected value + ), + mint_complex_0: ( + // In this example the "sent" value is 1.8 with 6 decimal places + // The mint value will be 3.6 with 12 decimal places + Uint128::new(2_000_000_000_000_000_000), + Uint128::new(1_800_000), + 6u8, + Uint128::new(1_000_000_000_000_000_000), + 12u8, + Uint128::new(3_600_000_000_000), + ), + mint_complex_1: ( + // In amount is 50.000 valued at 20 + // target price is 100$ with 6 decimals + Uint128::new(20_000_000_000_000_000_000), + Uint128::new(50_000), + 3u8, + Uint128::new(100_000_000_000_000_000_000), + 6u8, + Uint128::new(10_000_000), + ), + mint_complex_2: ( + // In amount is 10,000,000 valued at 100 + // Target price is $10 with 6 decimals + Uint128::new(1_000_000_000_000_000_000_000), + Uint128::new(100_000_000_000_000), + 8u8, + Uint128::new(10_000_000_000_000_000_000), + 6u8, + Uint128::new(100_000_000_000_000), + ), + /* + mint_overflow_0: ( + // In amount is 1,000,000,000,000,000,000,000,000 valued at 1,000 + // Target price is $5 with 6 decimals + Uint128::new(1_000_000_000_000_000_000_000), + Uint128::new(100_000_000_000_000_000_000_000_000_000_000), + 8u8, + Uint128::new(5_000_000_000_000_000_000), + 6u8, + Uint128::new(500_000_000_000_000_000_000_000_000_000_000_000), + ), + */ +} diff --git a/contracts/mock_band/src/contract.rs b/contracts/mock_band/src/contract.rs index 3456fc9b7..ba30f6496 100644 --- a/contracts/mock_band/src/contract.rs +++ b/contracts/mock_band/src/contract.rs @@ -1,7 +1,6 @@ -use cosmwasm_math_compat::Uint128; use cosmwasm_std::{ to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, StdError, - StdResult, Storage, + StdResult, Storage, Uint128, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/contracts/mock_secretswap_pair/src/contract.rs b/contracts/mock_secretswap_pair/src/contract.rs index ae8d14f1f..b56c727fe 100644 --- a/contracts/mock_secretswap_pair/src/contract.rs +++ b/contracts/mock_secretswap_pair/src/contract.rs @@ -1,7 +1,6 @@ -use cosmwasm_math_compat::Uint128; use cosmwasm_std::{ to_binary, Api, Binary, Env, Extern, HandleResponse, HumanAddr, InitResponse, Querier, - StdError, StdResult, Storage, + StdError, StdResult, Storage, Uint128, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/contracts/mock_sienna_pair/src/contract.rs b/contracts/mock_sienna_pair/src/contract.rs index 5df3ca4fb..cc7ad88a7 100644 --- a/contracts/mock_sienna_pair/src/contract.rs +++ b/contracts/mock_sienna_pair/src/contract.rs @@ -1,7 +1,6 @@ -use cosmwasm_math_compat::Uint128; use cosmwasm_std::{ to_binary, Api, Binary, Env, Extern, HandleResponse, HumanAddr, InitResponse, Querier, - StdError, StdResult, Storage, + StdError, StdResult, Storage, Uint128, }; use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; use schemars::JsonSchema; diff --git a/contracts/oracle/Cargo.toml b/contracts/oracle/Cargo.toml index 0fb1c1309..fa72c9972 100644 --- a/contracts/oracle/Cargo.toml +++ b/contracts/oracle/Cargo.toml @@ -39,4 +39,6 @@ shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol", fe schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } snafu = { version = "0.6.3" } -mockall = "0.10.2" + +[dev-dependencies] +fadroma = { branch = "v100", git = "https://github.com/hackbg/fadroma.git" } diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index 5ed186c7b..3639f3aec 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -4,7 +4,7 @@ pub mod query; pub mod state; #[cfg(test)] -mod test; +pub mod test; #[cfg(target_arch = "wasm32")] mod wasm { diff --git a/contracts/oracle/src/query.rs b/contracts/oracle/src/query.rs index 2691ce258..a58c1123f 100644 --- a/contracts/oracle/src/query.rs +++ b/contracts/oracle/src/query.rs @@ -1,6 +1,6 @@ use crate::state::{config_r, dex_pairs_r, index_r}; use cosmwasm_math_compat::{Uint128, Uint512}; -use cosmwasm_std::{Api, Extern, Querier, StdError, StdResult, Storage}; +use cosmwasm_std::{self, Api, Extern, Querier, StdError, StdResult, Storage}; use shade_protocol::{ band, dex, oracle::{IndexElement, QueryAnswer}, @@ -48,10 +48,10 @@ pub fn price( pub fn prices( deps: &Extern, symbols: Vec, -) -> StdResult> { +) -> StdResult> { let mut band_symbols = vec![]; let mut band_quotes = vec![]; - let mut results = vec![Uint128::zero(); symbols.len()]; + let mut results = vec![cosmwasm_std::Uint128::zero(); symbols.len()]; let config = config_r(&deps.storage).load()?; @@ -96,13 +96,14 @@ pub fn prices( results[result_index] = data.rate; } - Ok(results) + Ok(results.iter().map(|r| cosmwasm_std::Uint128(r.u128())).collect()) } pub fn eval_index( deps: &Extern, index: Vec, -) -> StdResult { +) -> StdResult { + let mut weight_sum = Uint512::zero(); let mut price = Uint512::zero(); @@ -160,7 +161,7 @@ pub fn eval_index( } } - Ok(Uint128::try_from( - price * Uint512::from(10u128.pow(18)) / weight_sum, - )?) + Ok(cosmwasm_std::Uint128( + Uint128::try_from(price.checked_mul(Uint512::from(10u128.pow(18)))?.checked_div(weight_sum)?)?.u128(), + )) } diff --git a/contracts/oracle/src/test.rs b/contracts/oracle/src/test.rs index 8fe6576f6..a194a544b 100644 --- a/contracts/oracle/src/test.rs +++ b/contracts/oracle/src/test.rs @@ -1,5 +1,48 @@ -#[cfg(test)] -mod tests { - use crate::contract; - use crate::query; +use crate::{ + contract::{handle, init, query} +}; +use cosmwasm_std::{ + coins, from_binary, + Extern, HumanAddr, StdError, + Binary, StdResult, HandleResponse, Env, + InitResponse, +}; +use fadroma::{ + ContractLink, + ensemble::{ + MockEnv, MockDeps, + ContractHarness, ContractEnsemble, + }, +}; + +pub struct Oracle; + +impl ContractHarness for Oracle { + // Use the method from the default implementation + fn init(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + init( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + fn handle(&self, deps: &mut MockDeps, env: Env, msg: Binary) -> StdResult { + handle( + deps, + env, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } + + // Override with some hardcoded value for the ease of testing + fn query(&self, deps: &MockDeps, msg: Binary) -> StdResult { + query( + deps, + from_binary(&msg)?, + //mint::DefaultImpl, + ) + } } diff --git a/contracts/rewards_emission/.cargo/config b/contracts/rewards_emission/.cargo/config new file mode 100644 index 000000000..882fe08f6 --- /dev/null +++ b/contracts/rewards_emission/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/rewards_emission/.circleci/config.yml b/contracts/rewards_emission/.circleci/config.yml new file mode 100644 index 000000000..127e1ae7d --- /dev/null +++ b/contracts/rewards_emission/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +jobs: + build: + docker: + - image: rust:1.43.1 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + - run: + name: Build + command: cargo wasm --locked + - run: + name: Unit tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Integration tests + command: cargo integration-test --locked + - run: + name: Format source code + command: cargo fmt + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in source code and schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + - target/wasm32-unknown-unknown/release/.fingerprint + - target/wasm32-unknown-unknown/release/build + - target/wasm32-unknown-unknown/release/deps + key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} diff --git a/contracts/rewards_emission/Cargo.toml b/contracts/rewards_emission/Cargo.toml new file mode 100644 index 000000000..411600555 --- /dev/null +++ b/contracts/rewards_emission/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "rewards_emission" +version = "0.1.0" +authors = ["Jack Swenson "] +edition = "2018" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +debug-print = ["cosmwasm-std/debug-print"] + +[dependencies] +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std", features = ["staking"] } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } +shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol", features = ["rewards_emission", "snip20"] } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +snafu = { version = "0.6.3" } diff --git a/contracts/rewards_emission/Makefile b/contracts/rewards_emission/Makefile new file mode 100644 index 000000000..2493c22f4 --- /dev/null +++ b/contracts/rewards_emission/Makefile @@ -0,0 +1,68 @@ +.PHONY: check +check: + cargo check + +.PHONY: clippy +clippy: + cargo clippy + +PHONY: test +test: unit-test + +.PHONY: unit-test +unit-test: + cargo test + +# This is a local build with debug-prints activated. Debug prints only show up +# in the local development chain (see the `start-server` command below) +# and mainnet won't accept contracts built with the feature enabled. +.PHONY: build _build +build: _build compress-wasm +_build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --features="debug-print" + +# This is a build suitable for uploading to mainnet. +# Calls to `debug_print` get removed by the compiler. +.PHONY: build-mainnet _build-mainnet +build-mainnet: _build-mainnet compress-wasm +_build-mainnet: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +# like build-mainnet, but slower and more deterministic +.PHONY: build-mainnet-reproducible +build-mainnet-reproducible: + docker run --rm -v "$$(pwd)":/contract \ + --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/contract/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + enigmampc/secret-contract-optimizer:1.0.3 + +.PHONY: compress-wasm +compress-wasm: + cp ./target/wasm32-unknown-unknown/release/*.wasm ./contract.wasm + @## The following line is not necessary, may work only on linux (extra size optimization) + @# wasm-opt -Os ./contract.wasm -o ./contract.wasm + cat ./contract.wasm | gzip -9 > ./contract.wasm.gz + +.PHONY: schema +schema: + cargo run --example schema + +# Run local development chain with four funded accounts (named a, b, c, and d) +.PHONY: start-server +start-server: # CTRL+C to stop + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1317:1317 \ + -v $$(pwd):/root/code \ + --name secretdev enigmampc/secret-network-sw-dev:v1.0.4-3 + +# This relies on running `start-server` in another console +# You can run other commands on the secretcli inside the dev image +# by using `docker exec secretdev secretcli`. +.PHONY: store-contract-local +store-contract-local: + docker exec secretdev secretcli tx compute store -y --from a --gas 1000000 /root/code/contract.wasm.gz + +.PHONY: clean +clean: + cargo clean + -rm -f ./contract.wasm ./contract.wasm.gz diff --git a/contracts/rewards_emission/README.md b/contracts/rewards_emission/README.md new file mode 100644 index 000000000..8f95041bb --- /dev/null +++ b/contracts/rewards_emission/README.md @@ -0,0 +1,65 @@ +# sSCRT Staking Contract +* [Introduction](#Introduction) +* [Sections](#Sections) + * [Init](#Init) + * [Admin](#Admin) + * Messages + * [UpdateConfig](#UpdateConfig) + * [Receive](#Receive) + * [Unbond](#Unbond) + * [Claim](#Claim) + * Queries + * [GetConfig](#GetConfig) + * [Delegations](#Delegations) + * [Delegation](#Delegation) +# Introduction +The sSCRT Staking contract receives sSCRT, redeems it for SCRT, then stakes it with a validator that falls within the criteria it has been configured with. The configured `treasury` will receive all funds from claiming rewards/unbonding. + +# Sections + +## Init +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|owner | HumanAddr | contract owner/admin; a valid bech32 address; +|treasury | HumanAddre | contract designated to receive all outgoing funds +|sscrt | Contract | sSCRT Snip-20 contract to accept for redemption/staking, all other funds will error +|validator_bounds | ValidatorBounds | criteria defining an acceptable validator to stake with + +## Admin + +### Messages +#### UpdateConfig +Updates the given values +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|owner | HumanAddr | contract owner/admin; a valid bech32 address; +|treasury | HumanAddre | contract designated to receive all outgoing funds +|sscrt | Contract | sSCRT Snip-20 contract to accept for redemption/staking, all other funds will error +|validator_bounds | ValidatorBounds | criteria defining an acceptable validator to stake with + +##### Response +```json +{ + "update_config": { + "status": "success" + } +} +``` + + +### Queries + +#### GetConfig +Gets the contract's configuration variables +##### Response +```json +{ + "config": { + "config": { + "owner": "Owner address", + } + } +} +``` diff --git a/contracts/rewards_emission/src/contract.rs b/contracts/rewards_emission/src/contract.rs new file mode 100644 index 000000000..1177aad83 --- /dev/null +++ b/contracts/rewards_emission/src/contract.rs @@ -0,0 +1,103 @@ +use cosmwasm_std::{ + debug_print, to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, + StdResult, StdError, + Storage, Uint128, +}; + +use shade_protocol::{ + adapter, + rewards_emission::{Config, HandleMsg, InitMsg, QueryMsg}, +}; + +use secret_toolkit::snip20::{register_receive_msg, set_viewing_key_msg}; + +use crate::{ + handle, query, + state::{ + config_w, self_address_w, + viewing_key_r, viewing_key_w, + }, +}; + +pub fn init( + deps: &mut Extern, + env: Env, + msg: InitMsg, +) -> StdResult { + + let mut config = msg.config; + + if !config.admins.contains(&env.message.sender) { + config.admins.push(env.message.sender); + } + + config_w(&mut deps.storage).save(&config)?; + + self_address_w(&mut deps.storage).save(&env.contract.address)?; + viewing_key_w(&mut deps.storage).save(&msg.viewing_key)?; + + Ok(InitResponse { + messages: vec![], + log: vec![], + }) +} + +pub fn handle( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + match msg { + HandleMsg::Receive { + sender, + from, + amount, + msg, + .. + } => handle::receive(deps, env, sender, from, amount, msg), + HandleMsg::UpdateConfig { config } => handle::try_update_config(deps, env, config), + HandleMsg::RegisterAsset { + asset, + } => handle::register_asset(deps, env, &asset), + HandleMsg::RefillRewards { + rewards, + } => handle::refill_rewards(deps, env, rewards), + + HandleMsg::Adapter(adapter) => match adapter { + // Maybe should return an Ok still? + adapter::SubHandleMsg::Unbond { asset, amount } => Err(StdError::generic_err("Cannot unbond from rewards")), + // If error on unbond, also error on claim + adapter::SubHandleMsg::Claim { asset } => handle::claim(deps, env, asset), + adapter::SubHandleMsg::Update { asset } => handle::update(deps, env, asset), + }, + } +} + +pub fn query( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&query::config(deps)?), + QueryMsg::PendingAllowance { asset } => to_binary(&query::pending_allowance(deps, asset)?), + QueryMsg::Adapter(adapter) => match adapter { + adapter::SubQueryMsg::Balance { asset } => to_binary(&query::balance(deps, asset)?), + // Unbonding disabled + adapter::SubQueryMsg::Claimable { asset } => to_binary( + &adapter::QueryAnswer::Claimable { + amount: Uint128::zero(), + } + ), + adapter::SubQueryMsg::Unbonding { asset } => to_binary( + &adapter::QueryAnswer::Unbonding { + amount: Uint128::zero(), + } + ), + adapter::SubQueryMsg::Unbondable { asset } => to_binary( + &adapter::QueryAnswer::Unbondable { + amount: Uint128::zero(), + } + ), + }, + } +} diff --git a/contracts/rewards_emission/src/handle.rs b/contracts/rewards_emission/src/handle.rs new file mode 100644 index 000000000..c68ea8bc5 --- /dev/null +++ b/contracts/rewards_emission/src/handle.rs @@ -0,0 +1,216 @@ +use cosmwasm_std::{ + debug_print, to_binary, Api, BalanceResponse, BankQuery, Binary, Coin, CosmosMsg, Env, Extern, + HandleResponse, HumanAddr, Querier, StakingMsg, StdError, StdResult, Storage, Uint128, + Validator, +}; + +use secret_toolkit::snip20::{ + deposit_msg, redeem_msg, send_from_msg, + batch_send_from_msg, register_receive_msg, + set_viewing_key_msg, + batch::SendFromAction, +}; + +use shade_protocol::{ + rewards_emission::{ + Config, Reward, HandleAnswer, + }, + adapter, + utils::{ + generic_response::ResponseStatus, + asset::{ + Contract, + scrt_balance, + } + }, + snip20::{Snip20Asset, fetch_snip20}, +}; + +use crate::{ + query, + state::{ + config_r, config_w, + self_address_r, + asset_r, asset_w, + assets_w, + viewing_key_r, + }, +}; + +pub fn receive( + deps: &mut Extern, + env: Env, + _sender: HumanAddr, + _from: HumanAddr, + amount: Uint128, + _msg: Option, +) -> StdResult { + + //TODO: forward to distributor (quick fix mechanism) + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Receive { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_update_config( + deps: &mut Extern, + env: Env, + config: Config, +) -> StdResult { + let cur_config = config_r(&deps.storage).load()?; + + if !cur_config.admins.contains(&env.message.sender) { + return Err(StdError::Unauthorized { backtrace: None }); + } + + config_w(&mut deps.storage).save(&config)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn register_asset( + deps: &mut Extern, + env: Env, + contract: &Contract, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + if !config.admins.contains(&env.message.sender) { + return Err(StdError::unauthorized()); + } + + assets_w(&mut deps.storage).update(|mut list| { + if !list.contains(&contract.address) { + list.push(contract.address.clone()); + } + Ok(list) + })?; + + asset_w(&mut deps.storage).save( + contract.address.to_string().as_bytes(), + &fetch_snip20(contract, &deps.querier)?, + )?; + + Ok(HandleResponse { + messages: vec![ + // Register contract in asset + register_receive_msg( + env.contract_code_hash.clone(), + None, + 256, + contract.code_hash.clone(), + contract.address.clone(), + )?, + // Set viewing key + set_viewing_key_msg( + viewing_key_r(&deps.storage).load()?, + None, + 256, + contract.code_hash.clone(), + contract.address.clone(), + )?, + ], + log: vec![], + data: Some(to_binary(&HandleAnswer::RegisterAsset { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn refill_rewards( + deps: &mut Extern, + env: Env, + rewards: Vec, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + + if env.message.sender != config.distributor { + return Err(StdError::unauthorized()); + } + + let mut messages = vec![]; + + for reward in rewards { + + let full_asset = match asset_r(&deps.storage).may_load(&reward.asset.as_str().as_bytes())? { + Some(a) => a, + None => { + return Err(StdError::generic_err(format!("Unrecognized Asset {}", reward.asset))); + } + }; + + messages.push( + send_from_msg( + config.treasury.clone(), + config.distributor.clone(), + reward.amount, + None, + None, + None, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )? + ); + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&HandleAnswer::RefillRewards { + status: ResponseStatus::Success, + })?), + }) + +} + +pub fn update( + deps: &mut Extern, + env: Env, + asset: HumanAddr, +) -> StdResult { + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Update { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn claim( + deps: &mut Extern, + _env: Env, + asset: HumanAddr, +) -> StdResult { + + match asset_r(&deps.storage).may_load(&asset.as_str().as_bytes())? { + Some(_) => { + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Claim { + status: ResponseStatus::Success, + amount: Uint128::zero(), + })?), + }) + }, + None => { + Err(StdError::generic_err(format!("Unrecognized Asset {}", asset))) + } + } + +} diff --git a/contracts/rewards_emission/src/lib.rs b/contracts/rewards_emission/src/lib.rs new file mode 100644 index 000000000..5ed186c7b --- /dev/null +++ b/contracts/rewards_emission/src/lib.rs @@ -0,0 +1,44 @@ +pub mod contract; +pub mod handle; +pub mod query; +pub mod state; + +#[cfg(test)] +mod test; + +#[cfg(target_arch = "wasm32")] +mod wasm { + use super::contract; + use cosmwasm_std::{ + do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + }; + + #[no_mangle] + extern "C" fn init(env_ptr: u32, msg_ptr: u32) -> u32 { + do_init( + &contract::init::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn handle(env_ptr: u32, msg_ptr: u32) -> u32 { + do_handle( + &contract::handle::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn query(msg_ptr: u32) -> u32 { + do_query( + &contract::query::, + msg_ptr, + ) + } + + // Other C externs like cosmwasm_vm_version_1, allocate, deallocate are available + // automatically because we `use cosmwasm_std`. +} diff --git a/contracts/rewards_emission/src/query.rs b/contracts/rewards_emission/src/query.rs new file mode 100644 index 000000000..367b8395f --- /dev/null +++ b/contracts/rewards_emission/src/query.rs @@ -0,0 +1,83 @@ +use cosmwasm_std::{ + Api, BalanceResponse, BankQuery, Delegation, DistQuery, Extern, FullDelegation, HumanAddr, + Querier, RewardsResponse, StdError, StdResult, Storage, Uint128, +}; + +use shade_protocol::{ + adapter, + rewards_emission::QueryAnswer, + utils::asset::scrt_balance, +}; + +use secret_toolkit::snip20::{ + balance_query, + allowance_query, +}; + +use crate::state::{ + config_r, self_address_r, + viewing_key_r, + assets_r, + asset_r, +}; + +pub fn config(deps: &Extern) -> StdResult { + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) +} + +pub fn pending_allowance( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let full_asset = match asset_r(&deps.storage).may_load(asset.as_str().as_bytes())? { + Some(a) => a, + None => { + return Err(StdError::generic_err(format!("Unrecognized Asset {}", asset))); + } + }; + + let config = config_r(&deps.storage).load()?; + + let allowance = allowance_query( + &deps.querier, + config.treasury, + self_address_r(&deps.storage).load()?, + viewing_key_r(&deps.storage).load()?, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )?.allowance; + + Ok(QueryAnswer::PendingAllowance { + amount: allowance, + }) +} + +pub fn balance( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let full_asset = match asset_r(&deps.storage).may_load(asset.as_str().as_bytes())? { + Some(a) => a, + None => { + return Err(StdError::generic_err(format!("Unrecognized Asset {}", asset))); + } + }; + + let balance = balance_query( + &deps.querier, + self_address_r(&deps.storage).load()?, + viewing_key_r(&deps.storage).load()?, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )?.amount; + + Ok(adapter::QueryAnswer::Balance { + amount: balance, + }) +} diff --git a/contracts/rewards_emission/src/state.rs b/contracts/rewards_emission/src/state.rs new file mode 100644 index 000000000..27cbc1682 --- /dev/null +++ b/contracts/rewards_emission/src/state.rs @@ -0,0 +1,57 @@ +use cosmwasm_std::{HumanAddr, Storage, Uint128}; +use cosmwasm_storage::{ + singleton, singleton_read, + ReadonlySingleton, Singleton, + bucket, bucket_read, + ReadonlyBucket, Bucket, +}; +use shade_protocol::{ + rewards_emission, + snip20::Snip20Asset, +}; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static SELF_ADDRESS: &[u8] = b"self_address"; +pub static VIEWING_KEY: &[u8] = b"viewing_key"; +pub static ASSETS: &[u8] = b"assets"; +pub static ASSET: &[u8] = b"asset"; + +pub fn config_w(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn self_address_w(storage: &mut S) -> Singleton { + singleton(storage, SELF_ADDRESS) +} + +pub fn self_address_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, SELF_ADDRESS) +} + +pub fn viewing_key_w(storage: &mut S) -> Singleton { + singleton(storage, VIEWING_KEY) +} + +pub fn viewing_key_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, VIEWING_KEY) +} + +pub fn assets_r(storage: &S) -> ReadonlySingleton> { + singleton_read(storage, ASSETS) +} + +pub fn assets_w(storage: &mut S) -> Singleton> { + singleton(storage, ASSETS) +} + +pub fn asset_r(storage: &S) -> ReadonlyBucket { + bucket_read(ASSET, storage) +} + +pub fn asset_w(storage: &mut S) -> Bucket { + bucket(ASSET, storage) +} diff --git a/contracts/rewards_emission/src/test.rs b/contracts/rewards_emission/src/test.rs new file mode 100644 index 000000000..3e1406c89 --- /dev/null +++ b/contracts/rewards_emission/src/test.rs @@ -0,0 +1,46 @@ +/* +#[cfg(test)] +pub mod tests { + use cosmwasm_std::{ + testing::{ + mock_dependencies, mock_env, MockStorage, MockApi, MockQuerier + }, + HumanAddr, + coins, from_binary, StdError, Uint128, + Extern, + }; + use shade_protocol::{ + treasury::{ + QueryAnswer, InitMsg, HandleMsg, + QueryMsg, + }, + asset::Contract, + }; + + use crate::{ + contract::{ + init, handle, query, + }, + }; + + fn create_contract(address: &str, code_hash: &str) -> Contract { + let env = mock_env(address.to_string(), &[]); + return Contract{ + address: env.message.sender, + code_hash: code_hash.to_string() + } + } + + fn dummy_init(admin: String, viewing_key: String) -> Extern { + let mut deps = mock_dependencies(20, &[]); + let msg = InitMsg { + admin: Option::from(HumanAddr(admin.clone())), + viewing_key, + }; + let env = mock_env(admin, &coins(1000, "earth")); + let _res = init(&mut deps, env, msg).unwrap(); + + return deps + } +} +*/ diff --git a/contracts/scrt_staking/README.md b/contracts/scrt_staking/README.md index 8f95041bb..e8fbeb34e 100644 --- a/contracts/scrt_staking/README.md +++ b/contracts/scrt_staking/README.md @@ -2,16 +2,15 @@ * [Introduction](#Introduction) * [Sections](#Sections) * [Init](#Init) - * [Admin](#Admin) + * [DAO Adapter](/packages/shade_protocol/src/DAO_ADAPTER.md) + * [Interface](#Interface) * Messages - * [UpdateConfig](#UpdateConfig) * [Receive](#Receive) - * [Unbond](#Unbond) - * [Claim](#Claim) + * [UpdateConfig](#UpdateConfig) * Queries - * [GetConfig](#GetConfig) + * [Config](#Config) * [Delegations](#Delegations) - * [Delegation](#Delegation) + # Introduction The sSCRT Staking contract receives sSCRT, redeems it for SCRT, then stakes it with a validator that falls within the criteria it has been configured with. The configured `treasury` will receive all funds from claiming rewards/unbonding. @@ -21,12 +20,13 @@ The sSCRT Staking contract receives sSCRT, redeems it for SCRT, then stakes it w ##### Request |Name |Type |Description | optional | |----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| -|owner | HumanAddr | contract owner/admin; a valid bech32 address; -|treasury | HumanAddre | contract designated to receive all outgoing funds -|sscrt | Contract | sSCRT Snip-20 contract to accept for redemption/staking, all other funds will error +|admin | HumanAddr | contract owner/admin; a valid bech32 address; +|treasury | HumanAddr | contract designated to receive all outgoing funds +|sscrt | Contract | sSCRT Snip-20 contract to accept for redemption/staking, all other funds will error |validator_bounds | ValidatorBounds | criteria defining an acceptable validator to stake with +|viewing_key | String | Viewing Key to be set for any relevant SNIP-20 -## Admin +## Interface ### Messages #### UpdateConfig @@ -35,7 +35,7 @@ Updates the given values |Name |Type |Description | optional | |----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| |owner | HumanAddr | contract owner/admin; a valid bech32 address; -|treasury | HumanAddre | contract designated to receive all outgoing funds +|treasury | HumanAddr | contract designated to receive all outgoing funds |sscrt | Contract | sSCRT Snip-20 contract to accept for redemption/staking, all other funds will error |validator_bounds | ValidatorBounds | criteria defining an acceptable validator to stake with @@ -51,7 +51,7 @@ Updates the given values ### Queries -#### GetConfig +#### Config Gets the contract's configuration variables ##### Response ```json diff --git a/contracts/scrt_staking/src/contract.rs b/contracts/scrt_staking/src/contract.rs index 451ef6bef..9fdb63b93 100644 --- a/contracts/scrt_staking/src/contract.rs +++ b/contracts/scrt_staking/src/contract.rs @@ -1,15 +1,23 @@ use cosmwasm_std::{ debug_print, to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, - StdResult, Storage, + StdResult, StdError, + Storage, Uint128, }; -use shade_protocol::scrt_staking::{Config, HandleMsg, InitMsg, QueryMsg}; +use shade_protocol::{ + adapter, + scrt_staking::{Config, HandleMsg, InitMsg, QueryMsg}, +}; use secret_toolkit::snip20::{register_receive_msg, set_viewing_key_msg}; use crate::{ handle, query, - state::{config_w, self_address_w, viewing_key_r, viewing_key_w}, + state::{ + config_w, self_address_w, + viewing_key_r, viewing_key_w, + unbonding_w, + }, }; pub fn init( @@ -17,6 +25,7 @@ pub fn init( env: Env, msg: InitMsg, ) -> StdResult { + let config = Config { admin: match msg.admin { None => env.message.sender.clone(), @@ -31,6 +40,7 @@ pub fn init( self_address_w(&mut deps.storage).save(&env.contract.address)?; viewing_key_w(&mut deps.storage).save(&msg.viewing_key)?; + unbonding_w(&mut deps.storage).save(&Uint128::zero())?; debug_print!("Contract was initialized by {}", env.message.sender); @@ -67,12 +77,13 @@ pub fn handle( amount, msg, .. - } => handle::receive(deps, env, sender, from, amount.into(), msg), - HandleMsg::UpdateConfig { admin } => handle::try_update_config(deps, env, admin), - // Begin unbonding of a certain amount of scrt - HandleMsg::Unbond { validator } => handle::unbond(deps, env, validator), - // Collect a completed unbonding/rewards - HandleMsg::Claim { validator } => handle::claim(deps, env, validator), + } => handle::receive(deps, env, sender, from, amount, msg), + HandleMsg::UpdateConfig { config } => handle::try_update_config(deps, env, config), + HandleMsg::Adapter(adapter) => match adapter { + adapter::SubHandleMsg::Unbond { asset, amount } => handle::unbond(deps, env, asset, amount), + adapter::SubHandleMsg::Claim { asset } => handle::claim(deps, env, asset), + adapter::SubHandleMsg::Update { asset } => handle::update(deps, env, asset), + }, } } @@ -81,10 +92,13 @@ pub fn query( msg: QueryMsg, ) -> StdResult { match msg { - QueryMsg::GetConfig {} => to_binary(&query::config(deps)?), - // All delegations + QueryMsg::Config {} => to_binary(&query::config(deps)?), QueryMsg::Delegations {} => to_binary(&query::delegations(deps)?), - //QueryMsg::Delegation { validator } => to_binary(&query::delegation(deps, validator)?), - QueryMsg::Rewards {} => to_binary(&query::rewards(deps)?), + QueryMsg::Adapter(adapter) => match adapter { + adapter::SubQueryMsg::Balance { asset } => to_binary(&query::balance(deps, asset)?), + adapter::SubQueryMsg::Claimable { asset } => to_binary(&query::claimable(deps, asset)?), + adapter::SubQueryMsg::Unbonding { asset } => to_binary(&query::unbonding(deps, asset)?), + adapter::SubQueryMsg::Unbondable { asset } => to_binary(&query::unbondable(deps, asset)?), + } } } diff --git a/contracts/scrt_staking/src/handle.rs b/contracts/scrt_staking/src/handle.rs index 571332394..ffbc15338 100644 --- a/contracts/scrt_staking/src/handle.rs +++ b/contracts/scrt_staking/src/handle.rs @@ -4,17 +4,29 @@ use cosmwasm_std::{ Validator, }; -use secret_toolkit::snip20::{deposit_msg, redeem_msg, send_msg}; +use secret_toolkit::snip20::{deposit_msg, redeem_msg}; -use shade_protocol::utils::generic_response::ResponseStatus; use shade_protocol::{ - scrt_staking::{HandleAnswer, ValidatorBounds}, + scrt_staking::{HandleAnswer, ValidatorBounds, Config}, treasury::Flag, + adapter, + utils::{ + generic_response::ResponseStatus, + asset::{ + Contract, + scrt_balance, + }, + wrap::{wrap_and_send, unwrap}, + }, }; use crate::{ query, - state::{config_r, config_w, self_address_r}, + state::{ + config_r, config_w, + self_address_r, + unbonding_w, unbonding_r, + }, }; pub fn receive( @@ -29,11 +41,8 @@ pub fn receive( let config = config_r(&deps.storage).load()?; - if config.sscrt.address != env.message.sender { - return Err(StdError::GenericErr { - msg: "Only accepts sSCRT".to_string(), - backtrace: None, - }); + if env.message.sender != config.sscrt.address { + return Err(StdError::generic_err("Only accepts sSCRT")); } let validator = choose_validator(&deps, env.block.time)?; @@ -67,22 +76,16 @@ pub fn receive( pub fn try_update_config( deps: &mut Extern, env: Env, - admin: Option, + config: Config, ) -> StdResult { - let config = config_r(&deps.storage).load()?; + let cur_config = config_r(&deps.storage).load()?; - if env.message.sender != config.admin { + if env.message.sender != cur_config.admin { return Err(StdError::Unauthorized { backtrace: None }); } // Save new info - let mut config = config_w(&mut deps.storage); - config.update(|mut state| { - if let Some(admin) = admin { - state.admin = admin; - } - Ok(state) - })?; + config_w(&mut deps.storage).save(&config)?; Ok(HandleResponse { messages: vec![], @@ -93,10 +96,69 @@ pub fn try_update_config( }) } +/* Claim rewards and restake, hold enough for pending unbondings + * Send available unbonded funds to treasury + */ +pub fn update( + deps: &mut Extern, + env: Env, + asset: HumanAddr, +) -> StdResult { + + let mut messages = vec![]; + + let config = config_r(&deps.storage).load()?; + + if asset != config.sscrt.address { + return Err(StdError::generic_err("Unrecognized Asset")); + } + + let scrt_balance = scrt_balance(deps, self_address_r(&deps.storage).load()?)?; + + // Claim Rewards + let rewards = query::rewards(&deps)?; + if rewards >= Uint128::zero() { + messages.append(&mut withdraw_rewards(deps)?); + } + + let mut stake_amount = rewards + scrt_balance; + let unbonding = unbonding_r(&deps.storage).load()?; + + // Don't restake funds that unbonded + if unbonding < stake_amount { + stake_amount = (stake_amount - unbonding)?; + } + else { + stake_amount = Uint128::zero(); + } + + if stake_amount > Uint128::zero() { + let validator = choose_validator(&deps, env.block.time)?; + messages.push( + CosmosMsg::Staking(StakingMsg::Delegate { + validator: validator.address.clone(), + amount: Coin { + amount: stake_amount, + denom: "uscrt".to_string(), + }, + }), + ); + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Update { + status: ResponseStatus::Success, + })?), + }) +} + pub fn unbond( deps: &mut Extern, env: Env, - validator: HumanAddr, + asset: HumanAddr, + amount: Uint128, ) -> StdResult { /* Unbonding to the scrt staking contract * Once scrt is on balance sheet, treasury can claim @@ -105,121 +167,223 @@ pub fn unbond( let config = config_r(&deps.storage).load()?; + //TODO: needs treasury & manager as admin, maybe just manager? + /* if env.message.sender != config.admin && env.message.sender != config.treasury { return Err(StdError::Unauthorized { backtrace: None }); } + */ - for delegation in deps - .querier - .query_all_delegations(self_address_r(&deps.storage).load()?)? - { - if delegation.validator == validator { - return Ok(HandleResponse { - messages: vec![CosmosMsg::Staking(StakingMsg::Undelegate { - validator, - amount: delegation.amount.clone(), - })], - log: vec![], - data: Some(to_binary(&HandleAnswer::Unbond { - status: ResponseStatus::Success, - delegation, - })?), - }); - } + if asset != config.sscrt.address { + return Err(StdError::generic_err("Unrecognized Asset")); + } + + let self_address = self_address_r(&deps.storage).load()?; + let delegations = query::delegations(&deps)?; + + let delegated = Uint128(delegations.iter() + .map(|d| d.amount.amount.u128()) + .sum::()); + let scrt_balance = scrt_balance(&deps, self_address)?; + let rewards = query::rewards(deps)?; + + let unbonding = unbonding_r(&deps.storage).load()?; + + // TODO: Refine this if we can query unbonding amounts + if delegated < amount { + return Err(StdError::generic_err( + format!("Unbond amount {} greater than delegated {}; rew {}, bal {}", + amount, delegated, rewards, scrt_balance) + )); } /* - if let Some(delegation) = deps.querier.query_delegation(env.contract.address, validator.clone())? { - - return Ok(HandleResponse { - messages: vec![ - CosmosMsg::Staking(StakingMsg::Undelegate { - validator, - amount: delegation.amount.clone(), - }), - ], - log: vec![], - data: Some(to_binary(&HandleAnswer::Unbond { - status: ResponseStatus::Success, - delegation, - })?), - }); + if amount > (scrt_balance + rewards + delegated) { + return Err(StdError::generic_err( + format!("Unbond {} greater than balance {}, rewards {}, del {}", + amount, scrt_balance, rewards, delegated) + )); } */ - Err(StdError::GenericErr { - msg: "No delegation to given validator".to_string(), - backtrace: None, + unbonding_w(&mut deps.storage).update(|u| Ok(u + amount))?; + + let mut messages = vec![]; + let mut undelegated = vec![]; + + let mut available = scrt_balance + rewards + delegated; + + if unbonding < available { + available = (available - unbonding)?; + } + else { + available = Uint128::zero(); + } + + if amount > available { + return Err(StdError::generic_err(format!("Cannot unbond more than is available: {}", available))); + } + let mut unbond_amount = amount; + + while unbond_amount > Uint128::zero() { + + // Unbond from largest validator first + let max_delegation = delegations.iter().max_by_key(|d| { + if undelegated.contains(&d.validator) { + Uint128::zero() + } + else { + d.amount.amount + } + }); + + // No more delegated funds to unbond + match max_delegation { + None => { + break; + } + Some(delegation) => { + + if undelegated.contains(&delegation.validator) + || delegation.amount.amount.clone() == Uint128::zero() { + break; + } + + // This delegation isn't enough to fully unbond + if delegation.amount.amount.clone() < unbond_amount { + messages.push( + CosmosMsg::Staking( + StakingMsg::Undelegate { + validator: delegation.validator.clone(), + amount: delegation.amount.clone(), + } + ) + ); + unbond_amount = (unbond_amount - delegation.amount.amount.clone())?; + } + else { + messages.push( + CosmosMsg::Staking( + StakingMsg::Undelegate { + validator: delegation.validator.clone(), + amount: Coin { + denom: delegation.amount.denom.clone(), + amount: unbond_amount, + } + } + ) + ); + unbond_amount = Uint128::zero(); + } + + undelegated.push(delegation.validator.clone()); + } + } + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Unbond { + status: ResponseStatus::Success, + amount: unbond_amount, + })?), }) } -/* - * Claims rewards and collects completed unbondings - * from a given validator and returns them directly to treasury - * - * TODO: convert to sSCRT first or rely on treasury to do so +pub fn withdraw_rewards( + deps: &mut Extern, +) -> StdResult> { + + let mut messages = vec![]; + let address = self_address_r(&deps.storage).load()?; + + for delegation in deps.querier.query_all_delegations(address.clone())? { + messages.push( + CosmosMsg::Staking( + StakingMsg::Withdraw { + validator: delegation.validator, + recipient: Some(address.clone()), + } + ) + ); + } + + Ok(messages) +} + +pub fn unwrap_and_stake( + _deps: &mut Extern, + amount: Uint128, + validator: Validator, + token: Contract, +) -> StdResult> { + + Ok(vec![ + // unwrap + unwrap(amount, token.clone())?, + // Stake + CosmosMsg::Staking(StakingMsg::Delegate { + validator: validator.address.clone(), + amount: Coin { + amount, + denom: "uscrt".to_string(), + }, + }), + ]) +} + +/* Claims completed unbondings, wraps them, + * and returns them to treasury */ pub fn claim( deps: &mut Extern, _env: Env, - validator: HumanAddr, + asset: HumanAddr, ) -> StdResult { let config = config_r(&deps.storage).load()?; - //TODO: query scrt balance and deposit into sscrt + if asset != config.sscrt.address { + return Err(StdError::generic_err("Unrecognized Asset")); + } let mut messages = vec![]; - let address = self_address_r(&deps.storage).load()?; + //let address = self_address_r(&deps.storage).load()?; - // Get total scrt balance, to get recently claimed rewards + lingering unbonded scrt - let scrt_balance: BalanceResponse = deps.querier.query( - &BankQuery::Balance { - address: address.clone(), - denom: "uscrt".to_string(), + let unbond_amount = unbonding_r(&deps.storage).load()?; + let mut claim_amount = Uint128::zero(); + + let scrt_balance = scrt_balance(deps, self_address_r(&deps.storage).load()?)?; + + if scrt_balance >= unbond_amount { + claim_amount = unbond_amount; + } + else { + // Claim Rewards + let rewards = query::rewards(&deps)?; + + if rewards >= Uint128::zero() { + messages.append(&mut withdraw_rewards(deps)?); } - .into(), - )?; - - let amount = query::rewards(&deps)? + scrt_balance.amount.amount; - - messages.push(CosmosMsg::Staking(StakingMsg::Withdraw { - validator, - recipient: Some(address.clone()), - })); - - messages.push(deposit_msg( - amount, - None, - 256, - config.sscrt.code_hash.clone(), - config.sscrt.address.clone(), - )?); - - /* NOTE: This will likely trigger the receive callback which - * would result in re-delegating a portion of the funds. - * This case will need to be tested and mitigated by either - * - accounting for it when rebalancing - * - add a "unallocated" flag with funds to force treasury not to - * allocate them, to then be allocated at rebalancing - */ - messages.push(send_msg( - config.treasury, - amount, - Some(to_binary(&Flag { - flag: "unallocated".to_string(), - })?), - None, - None, - 1, - config.sscrt.code_hash.clone(), - config.sscrt.address.clone(), - )?); + + if rewards + scrt_balance >= unbond_amount { + claim_amount = unbond_amount; + } + else { + claim_amount = rewards + scrt_balance; + } + } + + unbonding_w(&mut deps.storage).update(|u| Ok((u - claim_amount)?))?; + + messages.append(&mut wrap_and_send(claim_amount, config.treasury, config.sscrt, None)?); Ok(HandleResponse { messages, log: vec![], - data: Some(to_binary(&HandleAnswer::Claim { + data: Some(to_binary(&adapter::HandleAnswer::Claim { status: ResponseStatus::Success, + amount: claim_amount, })?), }) } @@ -228,25 +392,26 @@ pub fn choose_validator( deps: &Extern, seed: u64, ) -> StdResult { + let mut validators = deps.querier.query_validators()?; - let bounds = (config_r(&deps.storage).load()?).validator_bounds; // filter down to viable candidates - if let Some(bounds) = bounds { + if let Some(bounds) = (config_r(&deps.storage).load()?).validator_bounds { + let mut candidates = vec![]; + for validator in validators { + if is_validator_inbounds(&validator, &bounds) { candidates.push(validator); } } + validators = candidates; } if validators.is_empty() { - return Err(StdError::GenericErr { - msg: "No validators within bounds".to_string(), - backtrace: None, - }); + return Err(StdError::generic_err("No validators within bounds")); } // seed will likely be env.block.time @@ -254,5 +419,6 @@ pub fn choose_validator( } pub fn is_validator_inbounds(validator: &Validator, bounds: &ValidatorBounds) -> bool { - validator.commission <= bounds.max_commission && validator.commission >= bounds.min_commission + validator.commission <= bounds.max_commission + && validator.commission >= bounds.min_commission } diff --git a/contracts/scrt_staking/src/query.rs b/contracts/scrt_staking/src/query.rs index c2d5282a4..f171d0711 100644 --- a/contracts/scrt_staking/src/query.rs +++ b/contracts/scrt_staking/src/query.rs @@ -3,9 +3,9 @@ use cosmwasm_std::{ Querier, RewardsResponse, StdError, StdResult, Storage, Uint128, }; -use shade_protocol::scrt_staking::QueryAnswer; +use shade_protocol::{adapter, scrt_staking::QueryAnswer, utils::asset::scrt_balance}; -use crate::state::{config_r, self_address_r}; +use crate::state::{config_r, self_address_r, unbonding_r}; pub fn config(deps: &Extern) -> StdResult { Ok(QueryAnswer::Config { @@ -16,22 +16,15 @@ pub fn config(deps: &Extern) -> StdResu pub fn delegations( deps: &Extern, ) -> StdResult> { - deps.querier - .query_all_delegations(self_address_r(&deps.storage).load()?) + + deps.querier.query_all_delegations( + self_address_r(&deps.storage).load()? + ) } -// TODO: change to 'claimable' pub fn rewards(deps: &Extern) -> StdResult { - let scrt_balance: BalanceResponse = deps.querier.query( - &BankQuery::Balance { - address: self_address_r(&deps.storage).load()?, - denom: "uscrt".to_string(), - } - .into(), - )?; - let query_rewards: RewardsResponse = deps - .querier + let query_rewards: RewardsResponse = deps.querier .query( &DistQuery::Rewards { delegator: self_address_r(&deps.storage).load()?, @@ -44,11 +37,11 @@ pub fn rewards(deps: &Extern) -> StdRes }); if query_rewards.total.is_empty() { - return Ok(scrt_balance.amount.amount); + return Ok(Uint128::zero()); } let denom = query_rewards.total[0].denom.as_str(); - query_rewards.total.iter().fold(Ok(Uint128(0)), |racc, d| { + query_rewards.total.iter().fold(Ok(Uint128::zero()), |racc, d| { let acc = racc?; if d.denom.as_str() != denom { Err(StdError::generic_err(format!( @@ -56,12 +49,107 @@ pub fn rewards(deps: &Extern) -> StdRes denom, &d.denom ))) } else { - Ok(acc + d.amount + scrt_balance.amount.amount) + Ok(acc + d.amount) + } + }) +} + +pub fn balance( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + if asset != config.sscrt.address { + return Err(StdError::generic_err(format!("Unrecognized Asset {}", asset))); + } + + let delegated = Uint128(delegations(deps)?.into_iter() + .map(|d| d.amount.amount.u128()) + .sum::()); + + let rewards = rewards(deps)?; + + Ok(adapter::QueryAnswer::Balance { + amount: delegated + rewards, + }) +} + +pub fn claimable( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + + if asset != config.sscrt.address { + return Err(StdError::generic_err(format!("Unrecognized Asset {}", asset))); + } + + let scrt_balance: BalanceResponse = deps.querier.query( + &BankQuery::Balance { + address: self_address_r(&deps.storage).load()?, + denom: "uscrt".to_string(), } + .into(), + )?; + + let mut amount = scrt_balance.amount.amount; + let unbonding = unbonding_r(&deps.storage).load()?; + + if amount > unbonding { + amount = unbonding; + } + + Ok(adapter::QueryAnswer::Claimable { + amount: amount, + }) +} + +pub fn unbonding( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + + if asset != config.sscrt.address { + return Err(StdError::generic_err(format!("Unrecognized Asset {}", asset))); + } + + Ok(adapter::QueryAnswer::Unbonding { + amount: unbonding_r(&deps.storage).load()? + }) +} + +pub fn unbondable( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + + if asset != config.sscrt.address { + return Err(StdError::generic_err(format!("Unrecognized Asset {}", asset))); + } + + let unbondable = match balance(deps, asset)? { + adapter::QueryAnswer::Balance { amount } => amount, + _ => { + return Err(StdError::generic_err("Failed to query balance")); + } + }; + + /*TODO: Query current unbondings + * u >= 7 = false + * u < 7 = true + */ + Ok(adapter::QueryAnswer::Unbondable { + amount: unbondable, }) } -// This won't work until cosmwasm 0.16ish +// This won't work until cosmwasm 0.16 /* pub fn delegation( deps: &Extern, diff --git a/contracts/scrt_staking/src/state.rs b/contracts/scrt_staking/src/state.rs index d5f496c76..4a3527e65 100644 --- a/contracts/scrt_staking/src/state.rs +++ b/contracts/scrt_staking/src/state.rs @@ -1,12 +1,11 @@ -use cosmwasm_std::{HumanAddr, Storage}; +use cosmwasm_std::{HumanAddr, Storage, Uint128}; use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; use shade_protocol::scrt_staking; pub static CONFIG_KEY: &[u8] = b"config"; pub static SELF_ADDRESS: &[u8] = b"self_address"; pub static VIEWING_KEY: &[u8] = b"viewing_key"; - -//pub static DELEGATIONS: &[u8] = b"delegations"; +pub static UNBONDING: &[u8] = b"unbonding"; pub fn config_w(storage: &mut S) -> Singleton { singleton(storage, CONFIG_KEY) @@ -32,12 +31,10 @@ pub fn viewing_key_r(storage: &S) -> ReadonlySingleton { singleton_read(storage, VIEWING_KEY) } -/* -pub fn delegations_r(storage: &S) -> ReadonlySingleton> { - singleton_read(storage, DELEGATIONS) +pub fn unbonding_w(storage: &mut S) -> Singleton { + singleton(storage, UNBONDING) } -pub fn delegations_w(storage: &mut S) -> Singleton> { - singleton(storage, DELEGATIONS) +pub fn unbonding_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, UNBONDING) } -*/ diff --git a/contracts/treasury/README.md b/contracts/treasury/README.md index 53fe5bcc9..b9d1b8c64 100644 --- a/contracts/treasury/README.md +++ b/contracts/treasury/README.md @@ -1,14 +1,24 @@ -# Treasury Contract +# Treasury * [Introduction](#Introduction) * [Sections](#Sections) * [Init](#Init) - * [Admin](#Admin) + * [DAO Adapter](/packages/shade_protocol/src/DAO_ADAPTER.md) + * [Interface](#Interface) * Messages + * [Receive](#Receive) * [UpdateConfig](#UpdateConfig) * [RegisterAsset](#RegisterAsset) + * [RegisterManager](#RegisterManager) + * [Allowance](#Allowance) + * [AddAccount](#AddAccount) + * [CloseAccount](#CloseAccount) * Queries - * [GetConfig](#GetConfig) - * [GetBalance](#GetBalance) + * [Config](#Config) + * [Assets](#Assets) + * [Allowances](#Allowances) + * [CurrentAllowances](#CurrentAllowances) + * [Allowance](#Allowance) + * [Account](#Account) # Introduction The treasury contract holds network funds from things such as mint commission and pending airdrop funds @@ -18,18 +28,21 @@ The treasury contract holds network funds from things such as mint commission an ##### Request |Name |Type |Description | optional | |----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| -|owner | string | contract owner/admin; a valid bech32 address; Controls funds +|admin | string | contract owner/admin; a valid bech32 address; Controls funds +|viewing_key | string | viewing key for all registered snip20 assets +|sscrt | Contract | sSCRT contract for wrapping & unwrapping -## Admin +## Interface ### Messages + #### UpdateConfig Updates the given values ##### Request |Name |Type |Description | optional | |----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| -|owner | string | New contract owner; SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | yes | -|oracle | Contract | Oracle contract | no | +|config | string | New config to be set for the contract + ##### Response ```json { @@ -40,7 +53,7 @@ Updates the given values ``` #### RegisterAsset -Registers a supported asset. The asset must be SNIP-20 compliant since [RegisterReceive](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md#RegisterReceive) is called. +Registers a SNIP-20 compliant asset since [RegisterReceive](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md#RegisterReceive) is called. Note: Will return an error if there's an asset with that address already registered. ##### Request @@ -58,27 +71,102 @@ Note: Will return an error if there's an asset with that address already registe ### Queries -#### GetConfig -Gets the contract's configuration variables +#### Config +Gets the contract's configuration ##### Response ```json { "config": { "config": { - "owner": "Owner address", + "admin": "admin address", + "sscrt": { + "address": "", + "code_hash": "", + }, } } } ``` -#### GetBalance -Get the treasury balance for a given snip20 asset -Note: Snip20 assets must be registered to have viewing key set +#### Assets +List of assets supported +##### Response +```json +{ + "assets": { + "assets": ["asset address", ...] + } +} +``` + +#### Allowances +List of configured allowances for things like treasury_manager & rewards +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | Asset to query balance of +##### Response +```json +{ + "allowances": { + "allowances": [ + { + "allowance": ... + }, + ...] + } +} +``` + +#### Allowance +List of configured allowances for things like treasury_manager & rewards +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | Asset to query allowance for +|spender | HumanAddr | Spender of allowance +##### Response +```json +{ + "allowances": { + "allowances": [ + { + "allowance": ... + }, + ... + ] + } +} +``` + +#### Accounts +List of account holders +##### Response +```json +{ + "accounts": { + "accounts": ["address0", ...], + } +} +``` + +#### Account +Balance of a given account holders assets (e.g. SHD staking) +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|holder | HumanAddr | Holder of the account +|asset | HumanAddr | Asset to query balance of ##### Response ```json { - "get_balance": { - "contract": "asset address", + "account": { + "account": { + "balances": Uint128, + "unbondings": Uint128, + "claimable": Uint128, + "status": ("active"|"disabled"|"closed"|"transferred"), + } } } ``` diff --git a/contracts/treasury/src/contract.rs b/contracts/treasury/src/contract.rs index fc5db109e..a8259b490 100644 --- a/contracts/treasury/src/contract.rs +++ b/contracts/treasury/src/contract.rs @@ -1,15 +1,19 @@ use cosmwasm_std::{ debug_print, to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, - StdResult, Storage, + StdResult, Storage, StdError, Uint128, }; -use shade_protocol::treasury::{Config, HandleMsg, InitMsg, QueryMsg}; +use shade_protocol::{ + adapter, + treasury::{Config, HandleMsg, InitMsg, QueryMsg}, +}; use crate::{ handle, query, state::{ - allocations_w, asset_list_w, config_w, last_allowance_refresh_w, self_address_w, - viewing_key_w, + allowances_w, asset_list_w, config_w, self_address_w, + viewing_key_w, managers_w, total_unbonding_w, + account_list_w, }, }; use chrono::prelude::*; @@ -19,6 +23,7 @@ pub fn init( env: Env, msg: InitMsg, ) -> StdResult { + config_w(&mut deps.storage).save(&Config { admin: msg.admin.unwrap_or(env.message.sender.clone()), sscrt: msg.sscrt, @@ -27,13 +32,8 @@ pub fn init( viewing_key_w(&mut deps.storage).save(&msg.viewing_key)?; self_address_w(&mut deps.storage).save(&env.contract.address)?; asset_list_w(&mut deps.storage).save(&Vec::new())?; - - //init last refresh with epoch 0 so first refresh always goes - let timestamp = 0; - let naive = NaiveDateTime::from_timestamp(timestamp, 0); - let datetime: DateTime = DateTime::from_utc(naive, Utc); - - last_allowance_refresh_w(&mut deps.storage).save(&datetime.to_rfc3339())?; + managers_w(&mut deps.storage).save(&Vec::new())?; + account_list_w(&mut deps.storage).save(&Vec::new())?; debug_print!("Contract was initialized by {}", env.message.sender); @@ -57,23 +57,16 @@ pub fn handle( .. } => handle::receive(deps, env, sender, from, amount, msg), HandleMsg::UpdateConfig { config } => handle::try_update_config(deps, env, config), - HandleMsg::RegisterAsset { contract, reserves } => { - handle::try_register_asset(deps, &env, &contract, reserves) + HandleMsg::RegisterAsset { contract, reserves } => handle::try_register_asset(deps, &env, &contract, reserves), + HandleMsg::RegisterManager { mut contract } => handle::register_manager(deps, &env, &mut contract ), + HandleMsg::Allowance { asset, allowance } => handle::allowance(deps, &env, asset, allowance), + HandleMsg::AddAccount { holder } => handle::add_account(deps, &env, holder), + HandleMsg::CloseAccount { holder } => handle::close_account(deps, &env, holder), + HandleMsg::Adapter(adapter) => match adapter { + adapter::SubHandleMsg::Update { asset } => handle::rebalance(deps, &env, asset), + adapter::SubHandleMsg::Claim { asset } => handle::claim(deps, &env, asset), + adapter::SubHandleMsg::Unbond { asset, amount } => handle::unbond(deps, &env, asset, amount), } - HandleMsg::RegisterAllocation { asset, allocation } => { - handle::register_allocation(deps, &env, asset, allocation) - } - HandleMsg::RefreshAllowance {} => handle::refresh_allowance(deps, &env), - HandleMsg::OneTimeAllowance { - asset, - spender, - amount, - expiration, - } => handle::one_time_allowance(deps, &env, asset, spender, amount, expiration), - /* - HandleMsg::Rebalance { - } => handle::rebalance(deps, &env), - */ } } @@ -84,11 +77,16 @@ pub fn query( match msg { QueryMsg::Config {} => to_binary(&query::config(deps)?), QueryMsg::Assets {} => to_binary(&query::assets(deps)?), - QueryMsg::Allocations { asset } => to_binary(&query::allocations(deps, asset)?), - QueryMsg::Balance { asset } => to_binary(&query::balance(&deps, &asset)?), - QueryMsg::Allowances { asset, spender } => { - to_binary(&query::allowances(&deps, &asset, &spender)?) + QueryMsg::Allowances { asset } => to_binary(&query::allowances(deps, asset)?), + QueryMsg::Allowance { asset, spender } => to_binary(&query::allowance(&deps, &asset, &spender)?), + QueryMsg::Accounts { } => to_binary(&query::accounts(&deps)?), + QueryMsg::Account { holder } => to_binary(&query::account(&deps, holder)?), + + QueryMsg::Adapter(adapter) => match adapter { + adapter::SubQueryMsg::Balance { asset } => to_binary(&query::balance(&deps, &asset)?), + adapter::SubQueryMsg::Unbonding { asset } => to_binary(&query::unbonding(&deps, &asset)?), + adapter::SubQueryMsg::Unbondable { asset } => to_binary(&StdError::generic_err("Not Implemented")), + adapter::SubQueryMsg::Claimable { asset } => to_binary(&query::claimable(&deps, &asset)?), } - QueryMsg::LastAllowanceRefresh {} => to_binary(&query::last_allowance_refresh(&deps)?), } } diff --git a/contracts/treasury/src/handle.rs b/contracts/treasury/src/handle.rs index 37282c22c..540bae212 100644 --- a/contracts/treasury/src/handle.rs +++ b/contracts/treasury/src/handle.rs @@ -1,26 +1,44 @@ -use cosmwasm_math_compat::Uint128; use cosmwasm_std; use cosmwasm_std::{ from_binary, to_binary, Api, Binary, CosmosMsg, Env, Extern, HandleResponse, HumanAddr, - Querier, StdError, StdResult, Storage, + Querier, StdError, StdResult, Storage, Uint128, }; -use secret_toolkit; -use secret_toolkit::snip20::{ - allowance_query, decrease_allowance_msg, increase_allowance_msg, register_receive_msg, - send_msg, set_viewing_key_msg, +use secret_toolkit::{ + snip20::{ + register_receive_msg, allowance_query, + decrease_allowance_msg, increase_allowance_msg, + set_viewing_key_msg, balance_query, + }, + utils::Query, }; use shade_protocol::{ snip20, - treasury::{Allocation, Config, Flag, HandleAnswer, QueryAnswer}, - utils::{asset::Contract, generic_response::ResponseStatus}, + adapter, + treasury::{ + Allowance, Config, Flag, Manager, Account, Status, + HandleAnswer, QueryAnswer, Balance, + }, + utils::{ + asset::Contract, + generic_response::ResponseStatus, + cycle::{ Cycle, parse_utc_datetime, exceeds_cycle }, + }, }; use crate::{ query, state::{ - allocations_r, allocations_w, asset_list_r, asset_list_w, assets_r, assets_w, config_r, - config_w, last_allowance_refresh_r, last_allowance_refresh_w, viewing_key_r, + allowances_r, allowances_w, + asset_list_r, asset_list_w, + assets_r, assets_w, + config_r, config_w, + viewing_key_r, self_address_r, + managers_r, managers_w, + account_r, account_w, + account_list_r, account_list_w, + total_unbonding_r, + total_unbonding_w, }, }; use chrono::prelude::*; @@ -28,77 +46,32 @@ use chrono::prelude::*; pub fn receive( deps: &mut Extern, env: Env, - _sender: HumanAddr, + sender: HumanAddr, _from: HumanAddr, amount: Uint128, msg: Option, ) -> StdResult { - //debug_print!("Treasured {} u{}", amount, asset.token_info.symbol); - // skip the rest if the send the "unallocated" flag - if let Some(f) = msg { - let flag: Flag = from_binary(&f)?; - // NOTE: would this be better as a non-exhaustive enum? - // https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute - if flag.flag == "unallocated" { - return Ok(HandleResponse { - messages: vec![], - log: vec![], - data: Some(to_binary(&HandleAnswer::Receive { - status: ResponseStatus::Success, - })?), + + let key = sender.as_str().as_bytes(); + + if let Some(mut account) = account_r(&deps.storage).may_load(&key)? { + + if let Some(i) = account.balances.iter() + .position(|b| b.token == env.message.sender) { + account.balances[i].amount += amount; + } + else { + account.balances.push(Balance { + token: env.message.sender, + amount, }); } - }; - let asset = assets_r(&deps.storage).load(env.message.sender.as_str().as_bytes())?; - - let messages = allocations_r(&deps.storage) - .may_load(asset.contract.address.as_str().as_bytes())? - .unwrap_or_default() - .into_iter() - .filter_map(|alloc| match alloc { - Allocation::Reserves { .. } | Allocation::Allowance { .. } => None, - Allocation::Rewards { - contract, - allocation, - } => Some(send_msg( - contract.address, - amount.multiply_ratio(allocation, 10u128.pow(18)).into(), - None, - None, - None, - 1, - asset.contract.code_hash.clone(), - asset.contract.address.clone(), - )), - Allocation::Staking { - contract, - allocation, - } => Some(send_msg( - contract.address, - amount.multiply_ratio(allocation, 10u128.pow(18)).into(), - None, - None, - None, - 1, - asset.contract.code_hash.clone(), - asset.contract.address.clone(), - )), - Allocation::Application { .. } => { - //debug_print!("Applications Unsupported {}/{} u{} to {}", allocation, amount, asset.token_info.symbol, contract.address); - //TODO: implement - None - } - Allocation::Pool { .. } => { - //debug_print!("Pools Unsupported {}/{} u{} to {}", allocation, amount, asset.token_info.symbol, contract.address); - //TODO: implement - None - } - }) - .collect::>()?; + account_w(&mut deps.storage).save(&key, &account)?; + } Ok(HandleResponse { - messages, + messages: vec![], log: vec![], data: Some(to_binary(&HandleAnswer::Receive { status: ResponseStatus::Success, @@ -128,139 +101,289 @@ pub fn try_update_config( }) } -pub fn refresh_allowance( +pub fn allowance_last_refresh( + deps: &Extern, + env: &Env, + allowance: &Allowance +) -> StdResult>> { + + // Parse previous refresh datetime + let rfc3339 = match allowance { + Allowance::Amount { last_refresh, .. } => last_refresh, + Allowance::Portion { last_refresh, .. } => last_refresh, + }; + + DateTime::parse_from_rfc3339(&rfc3339) + .map(|dt| Some(dt.with_timezone(&Utc))) + .map_err(|_| StdError::generic_err( + format!("Failed to parse datetime {}", rfc3339) + )) +} + +pub fn rebalance( deps: &mut Extern, env: &Env, + asset: HumanAddr, ) -> StdResult { + let naive = NaiveDateTime::from_timestamp(env.block.time as i64, 0); let now: DateTime = DateTime::from_utc(naive, Utc); - // Parse previous refresh datetime - let last_refresh = last_allowance_refresh_r(&deps.storage) - .load() - .and_then(|rfc3339| { - DateTime::parse_from_rfc3339(&rfc3339) - .map(|dt| dt.with_timezone(&Utc)) - .map_err(|_| StdError::generic_err("Failed to parse previous datetime")) - })?; - - // Fail if we have already refreshed this month - if now.year() <= last_refresh.year() && now.month() <= last_refresh.month() { - return Err(StdError::generic_err(format!( - "Last refresh too recent: {}", - last_refresh.to_rfc3339() - ))); + let key = viewing_key_r(&deps.storage).load()?; + let self_address = self_address_r(&deps.storage).load()?; + let mut messages = vec![]; + + let full_asset = match assets_r(&deps.storage).may_load(asset.as_str().as_bytes())? { + Some(a) => a, + None => { + return Err(StdError::generic_err("Not an asset")); + } + }; + let allowances = allowances_r(&deps.storage).load(asset.as_str().as_bytes())?; + + let balance = balance_query( + &deps.querier, + self_address, + key.clone(), + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )?.amount; + + let mut account_unbonding = Uint128::zero(); + + for holder in account_list_r(&deps.storage).load()? { + let account = account_r(&deps.storage).load(holder.as_str().as_bytes())?; + account_unbonding += Uint128(account.unbondings.iter() + .map(|u| { + if u.token == asset { u.amount.u128() } + else { 0u128 } + }).sum()); } - last_allowance_refresh_w(&mut deps.storage).save(&now.to_rfc3339())?; + let mut amount_total = Uint128::zero(); + let mut out_balance = Uint128::zero(); + + let mut managers = managers_r(&deps.storage).load()?; + + // Fetch & sum balances + for allowance in &allowances { + match allowance { + Allowance::Amount { + spender, + cycle, + amount, + last_refresh, + } => { + //TODO: Query allowance + amount_total += *amount; + }, + Allowance::Portion { + spender, + portion, + last_refresh, + tolerance, + } => { + //portion_total += *portion; + let i = managers.iter().position(|m| m.contract.address == *spender).unwrap(); + managers[i].balance = adapter::balance_query(&deps, + &full_asset.contract.address.clone(), + managers[i].contract.clone())?; + out_balance += managers[i].balance; + }, + } + } - Ok(HandleResponse { - messages: do_allowance_refresh(deps, env)?, - log: vec![], - data: Some(to_binary(&HandleAnswer::RefreshAllowance { - status: ResponseStatus::Success, - })?), - }) -} + let mut portion_total = ((balance + out_balance) - (amount_total + account_unbonding))?; -/* Not exposed as a tx - */ -pub fn do_allowance_refresh( - deps: &Extern, - env: &Env, -) -> StdResult> { - let mut messages = vec![]; + managers_w(&mut deps.storage).save(&managers)?; + let config = config_r(&deps.storage).load()?; - let key = viewing_key_r(&deps.storage).load()?; + // Perform rebalance + for allowance in allowances { + + match allowance { + + Allowance::Amount { + spender, + cycle, + amount, + last_refresh, + } => { + let datetime = parse_utc_datetime(&last_refresh)?; + + if exceeds_cycle(&datetime, &now, cycle) { + if let Some(msg) = set_allowance(&deps, env, + spender, amount, + key.clone(), full_asset.contract.clone())? { + messages.push(msg); + } + } + }, + Allowance::Portion { + spender, + portion, + last_refresh, + tolerance, + } => { + let desired_amount = portion_total.multiply_ratio( + portion, + 10u128.pow(18) + ); + + let threshold = (balance + out_balance) + .multiply_ratio(tolerance, 10u128.pow(18)); + + let adapter = managers.clone() + .into_iter() + .find(|m| m.contract.address == spender) + .unwrap(); - for asset in asset_list_r(&deps.storage).load()? { - for alloc in allocations_r(&deps.storage).load(asset.as_str().as_bytes())? { - if let Allocation::Allowance { address, amount } = alloc { - let full_asset = assets_r(&deps.storage).load(asset.as_str().as_bytes())?; - // Determine current allowance let cur_allowance = allowance_query( &deps.querier, env.contract.address.clone(), - address.clone(), + spender.clone(), key.clone(), 1, full_asset.contract.code_hash.clone(), full_asset.contract.address.clone(), - )? - .allowance - .into(); - - match amount.cmp(&cur_allowance) { - // decrease allowance - std::cmp::Ordering::Less => { - messages.push(decrease_allowance_msg( - address.clone(), - amount.checked_sub(cur_allowance)?.into(), - None, - None, - 1, - full_asset.contract.code_hash.clone(), - full_asset.contract.address.clone(), - )?); + )?.allowance; + + // UnderFunded + if cur_allowance + adapter.balance < desired_amount { + let increase = (desired_amount - (adapter.balance + cur_allowance))?; + if increase < threshold { + continue; } - // increase allowance - std::cmp::Ordering::Greater => { - messages.push(increase_allowance_msg( - address.clone(), - amount.checked_sub(cur_allowance)?.into(), + messages.push( + increase_allowance_msg( + spender, + increase, None, None, 1, full_asset.contract.code_hash.clone(), full_asset.contract.address.clone(), - )?); - } - _ => {} + )? + ); } - } - } - } - - Ok(messages) -} - -pub fn one_time_allowance( - deps: &mut Extern, - env: &Env, - asset: HumanAddr, - spender: HumanAddr, - amount: Uint128, - expiration: Option, -) -> StdResult { - let cur_config = config_r(&deps.storage).load()?; + // Overfunded + else if cur_allowance + adapter.balance > desired_amount { + let mut decrease = ((adapter.balance + cur_allowance) - desired_amount)?; + if decrease < threshold { + continue; + } - if env.message.sender != cur_config.admin { - return Err(StdError::unauthorized()); - } + // Remove allowance first + if cur_allowance > Uint128::zero() { + + if cur_allowance < decrease { + messages.push( + decrease_allowance_msg( + spender, + cur_allowance, + None, + None, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )? + ); + decrease = (decrease - cur_allowance)?; + } + else { + messages.push( + decrease_allowance_msg( + spender, + decrease, + None, + None, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )? + ); + decrease = Uint128::zero(); + } + } - let full_asset = assets_r(&deps.storage) - .may_load(asset.as_str().as_bytes())? - .ok_or_else(|| StdError::generic_err(format!("Unknown Asset: {}", asset)))?; + // Unbond remaining + if decrease > Uint128::zero() { - let messages = vec![increase_allowance_msg( - spender, - amount.into(), - expiration, - None, - 1, - full_asset.contract.code_hash.clone(), - full_asset.contract.address, - )?]; + messages.push( + adapter::unbond_msg( + asset.clone(), + decrease, + adapter.contract, + )? + ); + } + } + }, + } + }; Ok(HandleResponse { messages, log: vec![], - data: Some(to_binary(&HandleAnswer::OneTimeAllowance { + data: Some(to_binary(&HandleAnswer::Rebalance { status: ResponseStatus::Success, })?), }) } +pub fn set_allowance( + deps: &Extern, + env: &Env, + spender: HumanAddr, + amount: Uint128, + key: String, + asset: Contract, +) -> StdResult> { + + let cur_allowance = allowance_query( + &deps.querier, + env.contract.address.clone(), + spender.clone(), + key, + 1, + asset.code_hash.clone(), + asset.address.clone(), + )?; + + match amount.cmp(&cur_allowance.allowance) { + // Decrease Allowance + std::cmp::Ordering::Less => { + Ok(Some( + decrease_allowance_msg( + spender.clone(), + (cur_allowance.allowance - amount)?, + None, + None, + 1, + asset.code_hash.clone(), + asset.address.clone(), + )? + )) + }, + // Increase Allowance + std::cmp::Ordering::Greater => { + Ok(Some( + increase_allowance_msg( + spender.clone(), + (amount - cur_allowance.allowance)?, + None, + None, + 1, + asset.code_hash.clone(), + asset.address.clone(), + )? + )) + }, + _ => { Ok(None) } + } +} + pub fn try_register_asset( deps: &mut Extern, env: &Env, @@ -283,33 +406,61 @@ pub fn try_register_asset( &snip20::fetch_snip20(contract, &deps.querier)?, )?; - let allocs = reserves - .map(|r| vec![Allocation::Reserves { allocation: r }]) - .unwrap_or_default(); + allowances_w(&mut deps.storage).save(contract.address.as_str().as_bytes(), &Vec::new())?; + total_unbonding_w(&mut deps.storage).save(contract.address.as_str().as_bytes(), &Uint128::zero())?; + + Ok(HandleResponse { + messages: vec![ + // Register contract in asset + register_receive_msg( + env.contract_code_hash.clone(), + None, + 256, + contract.code_hash.clone(), + contract.address.clone(), + )?, + // Set viewing key + set_viewing_key_msg( + viewing_key_r(&deps.storage).load()?, + None, + 256, + contract.code_hash.clone(), + contract.address.clone(), + )?, + ], + log: vec![], + data: Some(to_binary(&HandleAnswer::RegisterAsset { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn register_manager( + deps: &mut Extern, + env: &Env, + contract: &mut Contract, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; - allocations_w(&mut deps.storage).save(contract.address.as_str().as_bytes(), &allocs)?; - - let messages = vec![ - // Register contract in asset - register_receive_msg( - env.contract_code_hash.clone(), - None, - 256, - contract.code_hash.clone(), - contract.address.clone(), - )?, - // Set viewing key - set_viewing_key_msg( - viewing_key_r(&deps.storage).load()?, - None, - 1, - contract.code_hash.clone(), - contract.address.clone(), - )?, - ]; + if env.message.sender != config.admin { + return Err(StdError::unauthorized()); + } + + managers_w(&mut deps.storage).update(|mut adapters| { + if adapters.iter().map(|m| m.contract.clone()).collect::>().contains(&contract) { + return Err(StdError::generic_err("Manager already registered")); + } + adapters.push(Manager { + contract: contract.clone(), + balance: Uint128::zero(), + desired: Uint128::zero(), + }); + Ok(adapters) + })?; Ok(HandleResponse { - messages, + messages: vec![], log: vec![], data: Some(to_binary(&HandleAnswer::RegisterAsset { status: ResponseStatus::Success, @@ -318,33 +469,34 @@ pub fn try_register_asset( } // extract contract address if any -fn allocation_address(allocation: &Allocation) -> Option<&HumanAddr> { - match allocation { - Allocation::Rewards { contract, .. } - | Allocation::Staking { contract, .. } - | Allocation::Application { contract, .. } - | Allocation::Pool { contract, .. } => Some(&contract.address), +fn allowance_address(allowance: &Allowance) -> Option<&HumanAddr> { + match allowance { + Allowance::Amount { spender, .. } => Some(&spender), + Allowance::Portion { spender, .. } => Some(&spender), _ => None, } } -// extract allocaiton portion -fn allocation_portion(allocation: &Allocation) -> u128 { - match allocation { - Allocation::Reserves { allocation } - | Allocation::Rewards { allocation, .. } - | Allocation::Staking { allocation, .. } - | Allocation::Application { allocation, .. } - | Allocation::Pool { allocation, .. } => allocation.u128(), - Allocation::Allowance { .. } => 0, +// extract allowanceaiton portion +fn allowance_portion(allowance: &Allowance) -> Uint128 { + match allowance { + Allowance::Portion { portion, .. } => *portion, + Allowance::Amount { .. } => Uint128::zero(), } } -pub fn register_allocation( +fn allowance_amount(allowance: &Allowance) -> Uint128 { + match allowance { + Allowance::Amount { amount, .. } => *amount, + Allowance::Portion { .. } => Uint128::zero(), + } +} + +pub fn allowance( deps: &mut Extern, env: &Env, asset: HumanAddr, - alloc: Allocation, + allowance: Allowance, ) -> StdResult { static ONE_HUNDRED_PERCENT: u128 = 10u128.pow(18); @@ -355,86 +507,298 @@ pub fn register_allocation( return Err(StdError::unauthorized()); } - // might be used later - let _full_asset = assets_r(&deps.storage) - .may_load(asset.to_string().as_bytes()) - .and_then(|asset| { - asset.ok_or_else(|| StdError::generic_err("Unexpected response for balance")) - })?; - - // might be used later - let _liquid_balance = query::balance(deps, &asset).and_then(|r| match r { - QueryAnswer::Balance { amount } => Ok(amount), - _ => Err(StdError::generic_err("Unexpected response for balance")), - })?; + let adapters = managers_r(&deps.storage).load()?; + + // Disallow Portion on non-adapters + match allowance { + Allowance::Portion { + ref spender, .. + } => { + if adapters.clone().into_iter().find(|m| m.contract.address == *spender).is_none() { + return Err(StdError::generic_err("Portion allowances to adapters only")); + } + } + _ => {} + }; let key = asset.as_str().as_bytes(); - let mut apps = allocations_r(&deps.storage) + let mut apps = allowances_r(&deps.storage) .may_load(key)? .unwrap_or_default(); - let alloc_address = allocation_address(&alloc); + let allow_address = allowance_address(&allowance); - // find any old allocations with the same contract address & sum current allocations in one loop. + // find any old allowances with the same contract address & sum current allowances in one loop. // saves looping twice in the worst case - let (stale_alloc, curr_alloc_portion) = + // TODO: Remove Reserves if this would be one of those + let (stale_allowance, cur_allowance_portion) = apps.iter() .enumerate() - .fold((None, 0u128), |(stale_alloc, curr_allocs), (idx, a)| { - if stale_alloc.is_none() && allocation_address(a) == alloc_address { - (Some(idx), curr_allocs) + .fold((None, 0u128), |(stale_allowance, cur_allowances), (idx, a)| { + if stale_allowance.is_none() && allowance_address(a) == allow_address { + (Some(idx), cur_allowances) } else { - (stale_alloc, curr_allocs + allocation_portion(a)) + (stale_allowance, cur_allowances + allowance_portion(a).u128()) } }); - if let Some(old_alloc_idx) = stale_alloc { - apps.remove(old_alloc_idx); + if let Some(old_allowance_idx) = stale_allowance { + apps.remove(old_allowance_idx); } - let new_alloc_portion = allocation_portion(&alloc); + let new_allowance_portion = allowance_portion(&allowance).u128(); - // NOTE: should this be '>' if 1e18 == 100%? - if curr_alloc_portion + new_alloc_portion >= ONE_HUNDRED_PERCENT { + if cur_allowance_portion + new_allowance_portion > ONE_HUNDRED_PERCENT { return Err(StdError::generic_err( - "Invalid allocation total exceeding 100%", + "Invalid allowance total exceeding 100%", )); } - apps.push(alloc); + // Zero the last-refresh + let datetime: DateTime = DateTime::from_utc( + NaiveDateTime::from_timestamp(0, 0), + Utc + ); + + let spender = match allowance { + + Allowance::Portion { + spender, portion, last_refresh, tolerance, + } => { + apps.push(Allowance::Portion { + spender: spender.clone(), + portion: portion.clone(), + last_refresh: datetime.to_rfc3339(), + tolerance, + }); + spender + }, + Allowance::Amount { + spender, + cycle, + amount, + last_refresh, + }=> { + apps.push(Allowance::Amount { + spender: spender.clone(), + cycle: cycle.clone(), + amount: amount.clone(), + last_refresh: datetime.to_rfc3339() + }); + spender + } + }; - allocations_w(&mut deps.storage).save(key, &apps)?; + allowances_w(&mut deps.storage).save(key, &apps)?; - /*TODO: Need to re-allocate/re-balance funds based on the new addition - * get Uint128 math functions to do these things (untested) - * re-add send_msg below - */ + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Allowance { + status: ResponseStatus::Success, + })?), + }) +} - /* - let liquid_portion = (allocated_portion * liquid_balance) / allocated_portion; +pub fn add_account( + deps: &mut Extern, + env: &Env, + holder: HumanAddr, +) -> StdResult { + + if env.message.sender != config_r(&deps.storage).load()?.admin { + return Err(StdError::unauthorized()); + } + + let key = holder.as_str().as_bytes(); + + account_list_w(&mut deps.storage).update(|mut accounts| { + if accounts.contains(&holder.clone()) { + return Err(StdError::generic_err("Account already exists")); + } + accounts.push(holder.clone()); + Ok(accounts) + })?; - // Determine how much of current balance is to be allocated - let to_allocate = liquid_balance - (alloc_portion / liquid_portion); + account_w(&mut deps.storage).save(key, + &Account { + balances: Vec::new(), + unbondings: Vec::new(), + claimable: Vec::new(), + status: Status::Active, + } + )?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::AddAccount { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn close_account( + deps: &mut Extern, + env: &Env, + holder: HumanAddr, +) -> StdResult { + + if env.message.sender != config_r(&deps.storage).load()?.admin { + return Err(StdError::unauthorized()); + } + + let key = holder.as_str().as_bytes(); + + if let Some(mut account) = account_r(&deps.storage).may_load(key)? { + account.status = Status::Closed; + account_w(&mut deps.storage).save(key, &account)?; + } else { + return Err(StdError::generic_err("Account doesn't exist")); + } + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::RemoveAccount { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn claim( + deps: &mut Extern, + env: &Env, + asset: HumanAddr, +) -> StdResult { + + if !account_list_r(&deps.storage).load()?.contains(&env.message.sender) { + return Err(StdError::unauthorized()); + } + + let key = asset.as_str().as_bytes(); + + let managers = managers_r(&deps.storage).load()?; + let allowances = allowances_r(&deps.storage).load(&key)?; + + let mut messages = vec![]; + + let mut claimed = Uint128::zero(); + + for allowance in allowances { + match allowance { + Allowance::Amount { .. } => {}, + Allowance::Portion { spender, .. } => { + if let Some(manager) = managers.iter().find(|m| m.contract.address == spender) { + + let claimable = adapter::claimable_query(&deps, &asset, manager.contract.clone())?; + + if claimable > Uint128::zero() { + messages.push( + adapter::claim_msg( + asset.clone(), + manager.contract.clone() + )? + ); + claimed += claimable; + } + } + } + } + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Claim { + status: ResponseStatus::Success, + amount: claimed, + })?), + }) +} + +pub fn unbond( + deps: &mut Extern, + env: &Env, + asset: HumanAddr, + amount: Uint128, +) -> StdResult { + + /* + if env.message.sender != config_r(&deps.storage).load()?.admin { + return Err(StdError::unauthorized()); + } */ + let account = match account_r(&deps.storage).may_load(&env.message.sender.as_str().as_bytes())? { + Some(a) => a, + None => { + return Err(StdError::unauthorized()); + } + }; + + let managers = managers_r(&deps.storage).load()?; + + let mut messages = vec![]; + + let mut unbond_amount = amount; + + for allowance in allowances_r(&deps.storage).load(asset.as_str().as_bytes())? { + match allowance { + Allowance::Amount { .. } => {}, + Allowance::Portion { spender, .. } => { + if let Some(manager) = managers.iter().find(|m| m.contract.address == spender) { + let balance = adapter::balance_query(&deps, &asset.clone(), manager.contract.clone())?; + + if balance > unbond_amount { + messages.push( + adapter::unbond_msg( + asset.clone(), + unbond_amount, + manager.contract.clone(), + )? + ); + unbond_amount = Uint128::zero(); + } + else { + messages.push( + adapter::unbond_msg( + asset.clone(), + balance, + manager.contract.clone(), + )? + ); + unbond_amount = (unbond_amount - balance)?; + } + } + } + } + + if unbond_amount == Uint128::zero() { + break; + } + } + + if unbond_amount > Uint128::zero() { + return Err(StdError::generic_err( + format!("Failed to fully unbond {}, {} available", + amount, (amount - unbond_amount)?) + )); + } + + total_unbonding_w(&mut deps.storage) + .update( + asset.as_str().as_bytes(), + |u| Ok(u.or(Some(Uint128::zero())).unwrap() + amount) + )?; + Ok(HandleResponse { - messages: vec![ - /* - send_msg( - alloc_address, - to_allocate, - None, - None, - 1, - full_asset.contract.code_hash.clone(), - full_asset.contract.address.clone(), - )? - */ - ], + messages, log: vec![], - data: Some(to_binary(&HandleAnswer::RegisterApp { + data: Some(to_binary(&adapter::HandleAnswer::Claim { status: ResponseStatus::Success, + amount, })?), }) } diff --git a/contracts/treasury/src/query.rs b/contracts/treasury/src/query.rs index 89f3abf08..b7cf4a175 100644 --- a/contracts/treasury/src/query.rs +++ b/contracts/treasury/src/query.rs @@ -1,10 +1,11 @@ -use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage}; -use secret_toolkit::{snip20::allowance_query, utils::Query}; -use shade_protocol::{snip20, treasury}; +use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage, Uint128}; +use secret_toolkit::{snip20::{allowance_query, balance_query}, utils::Query}; +use shade_protocol::{snip20, treasury, adapter}; use crate::state::{ - allocations_r, asset_list_r, assets_r, config_r, last_allowance_refresh_r, self_address_r, - viewing_key_r, + allowances_r, asset_list_r, assets_r, config_r, self_address_r, + viewing_key_r, managers_r, + account_list_r, account_r, }; pub fn config( @@ -18,26 +19,39 @@ pub fn config( pub fn balance( deps: &Extern, asset: &HumanAddr, -) -> StdResult { - //TODO: restrict to admin +) -> StdResult { + //TODO: restrict to admin? + + let managers = managers_r(&deps.storage).load()?; match assets_r(&deps.storage).may_load(asset.to_string().as_bytes())? { Some(a) => { - let resp = snip20::QueryMsg::Balance { - address: self_address_r(&deps.storage).load()?, - key: viewing_key_r(&deps.storage).load()?, - } - .query(&deps.querier, a.contract.code_hash, a.contract.address)?; - - match resp { - snip20::QueryAnswer::Balance { amount } => { - Ok(treasury::QueryAnswer::Balance { amount }) - } - _ => Err(StdError::GenericErr { - msg: "Unexpected Response".to_string(), - backtrace: None, - }), + let mut balance = balance_query( + &deps.querier, + self_address_r(&deps.storage).load()?, + viewing_key_r(&deps.storage).load()?, + 1, + a.contract.code_hash.clone(), + a.contract.address.clone(), + )?.amount; + + + for allowance in allowances_r(&deps.storage).load(&asset.as_str().as_bytes())? { + match allowance { + treasury::Allowance::Portion { spender, .. } => { + let manager = managers + .clone().into_iter() + .find(|m| m.contract.address == spender).unwrap(); + balance += adapter::balance_query( + &deps, + asset, + manager.contract + )?; + } + _ => {} + }; } + Ok(adapter::QueryAnswer::Balance { amount: balance }) } None => Err(StdError::NotFound { kind: asset.to_string(), @@ -46,11 +60,70 @@ pub fn balance( } } -pub fn allowances( +pub fn unbonding( + deps: &Extern, + asset: &HumanAddr, +) -> StdResult { + + let managers = managers_r(&deps.storage).load()?; + let mut unbonding = Uint128::zero(); + + for allowance in allowances_r(&deps.storage).load(&asset.as_str().as_bytes())? { + match allowance { + treasury::Allowance::Portion { spender, .. } => { + let manager = managers + .clone().into_iter() + .find(|m| m.contract.address == spender).unwrap(); + unbonding += adapter::unbonding_query( + &deps, + asset, + manager.contract + )?; + } + _ => {} + }; + } + + Ok(adapter::QueryAnswer::Unbonding { + amount: unbonding + }) +} + +pub fn claimable( + deps: &Extern, + asset: &HumanAddr, +) -> StdResult { + + let managers = managers_r(&deps.storage).load()?; + let mut claimable = Uint128::zero(); + + for allowance in allowances_r(&deps.storage).load(&asset.as_str().as_bytes())? { + match allowance { + treasury::Allowance::Portion { spender, .. } => { + let manager = managers + .clone().into_iter() + .find(|m| m.contract.address == spender).unwrap(); + claimable += adapter::claimable_query( + &deps, + asset, + manager.contract + )?; + } + _ => {} + }; + } + + Ok(adapter::QueryAnswer::Claimable { + amount: claimable + }) +} + +pub fn allowance( deps: &Extern, asset: &HumanAddr, spender: &HumanAddr, ) -> StdResult { + let self_address = self_address_r(&deps.storage).load()?; let key = viewing_key_r(&deps.storage).load()?; @@ -65,11 +138,8 @@ pub fn allowances( full_asset.contract.address.clone(), )?; - return Ok(treasury::QueryAnswer::Allowances { - allowances: vec![treasury::AllowanceData { - spender: spender.clone(), - amount: cur_allowance.allowance.into(), - }], + return Ok(treasury::QueryAnswer::Allowance { + allowance: cur_allowance.allowance, }); } @@ -84,30 +154,37 @@ pub fn assets( }) } -pub fn allocations( +pub fn allowances( deps: &Extern, asset: HumanAddr, ) -> StdResult { - Ok(treasury::QueryAnswer::Allocations { - allocations: match allocations_r(&deps.storage).may_load(asset.to_string().as_bytes())? { - None => { - vec![] - } + + Ok(treasury::QueryAnswer::Allowances { + allowances: match allowances_r(&deps.storage).may_load(asset.to_string().as_bytes())? { + None => vec![], Some(a) => a, }, }) } -pub fn last_allowance_refresh( +pub fn accounts( deps: &Extern, ) -> StdResult { - Ok(treasury::QueryAnswer::Allowances { allowances: vec![] }) + Ok(treasury::QueryAnswer::Accounts { + accounts: account_list_r(&deps.storage).load()?, + }) } -/* -pub fn can_rebalance( - _deps: &Extern, -) -> StdResult { - Ok(QueryAnswer::CanRebalance { possible: false }) +pub fn account( + deps: &Extern, + holder: HumanAddr, +) -> StdResult { + match account_r(&deps.storage).may_load(holder.as_str().as_bytes())? { + Some(a) => Ok( + treasury::QueryAnswer::Account { + account: a, + } + ), + None => Err(StdError::generic_err("Not an account holder")) + } } -*/ diff --git a/contracts/treasury/src/state.rs b/contracts/treasury/src/state.rs index 94e995069..4eedb114b 100644 --- a/contracts/treasury/src/state.rs +++ b/contracts/treasury/src/state.rs @@ -1,18 +1,25 @@ -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{HumanAddr, Storage}; +use cosmwasm_std::{HumanAddr, Storage, Uint128}; use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, - Singleton, + Singleton, +}; +use shade_protocol::{ + snip20::Snip20Asset, + utils::asset::Contract, + treasury }; -use shade_protocol::{snip20::Snip20Asset, treasury}; pub static CONFIG_KEY: &[u8] = b"config"; pub static ASSETS: &[u8] = b"assets"; pub static ASSET_LIST: &[u8] = b"asset_list"; pub static VIEWING_KEY: &[u8] = b"viewing_key"; pub static SELF_ADDRESS: &[u8] = b"self_address"; -pub static ALLOCATIONS: &[u8] = b"allocations"; -pub static ALLOWANCE_REFRESH: &[u8] = b"allowance_refresh"; +pub static ALLOWANCES: &[u8] = b"allowances"; +//pub static CUR_ALLOWANCES: &[u8] = b"allowances"; +pub static MANAGERS: &[u8] = b"managers"; +pub static ACCOUNT_LIST: &[u8] = b"account_list"; +pub static ACCOUNT: &[u8] = b"account"; +pub static UNBONDING: &[u8] = b"unbonding"; pub fn config_w(storage: &mut S) -> Singleton { singleton(storage, CONFIG_KEY) @@ -54,18 +61,61 @@ pub fn self_address_w(storage: &mut S) -> Singleton { singleton(storage, SELF_ADDRESS) } -pub fn allocations_r(storage: &S) -> ReadonlyBucket> { - bucket_read(ALLOCATIONS, storage) +pub fn allowances_r(storage: &S) -> ReadonlyBucket> { + bucket_read(ALLOWANCES, storage) +} + +pub fn allowances_w(storage: &mut S) -> Bucket> { + bucket(ALLOWANCES, storage) +} + +/* +pub fn current_allowances_r(storage: &S) -> ReadonlyBucket { + bucket_read(CUR_ALLOWANCES, storage) +} + +pub fn current_allowances_w(storage: &mut S) -> Bucket { + bucket(CUR_ALLOWANCES, storage) +} +*/ + +pub fn managers_r(storage: &S) -> ReadonlySingleton> { + singleton_read(storage, MANAGERS) +} + +pub fn managers_w(storage: &mut S) -> Singleton> { + singleton(storage, MANAGERS) +} + + +pub fn account_list_r(storage: &S) -> ReadonlySingleton> { + singleton_read(storage, ACCOUNT_LIST) +} +pub fn account_list_w(storage: &mut S) -> Singleton> { + singleton(storage, ACCOUNT_LIST) +} + +pub fn account_r(storage: &S) -> ReadonlyBucket { + bucket_read(ACCOUNT, storage) +} + +pub fn account_w(storage: &mut S) -> Bucket { + bucket(ACCOUNT, storage) +} + +// Total unbonding per asset, to be used in rebalance +pub fn total_unbonding_r(storage: &S) -> ReadonlyBucket { + bucket_read(UNBONDING, storage) } -pub fn allocations_w(storage: &mut S) -> Bucket> { - bucket(ALLOCATIONS, storage) +pub fn total_unbonding_w(storage: &mut S) -> Bucket { + bucket(UNBONDING, storage) } -pub fn last_allowance_refresh_r(storage: &S) -> ReadonlySingleton { - singleton_read(storage, ALLOWANCE_REFRESH) +pub fn unbondings_r(storage: &S) -> ReadonlyBucket { + bucket_read(UNBONDING, storage) } -pub fn last_allowance_refresh_w(storage: &mut S) -> Singleton { - singleton(storage, ALLOWANCE_REFRESH) +pub fn unbondings_w(storage: &mut S) -> Bucket { + bucket(UNBONDING, storage) } diff --git a/contracts/treasury_manager/.cargo/config b/contracts/treasury_manager/.cargo/config new file mode 100644 index 000000000..882fe08f6 --- /dev/null +++ b/contracts/treasury_manager/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/treasury_manager/.circleci/config.yml b/contracts/treasury_manager/.circleci/config.yml new file mode 100644 index 000000000..127e1ae7d --- /dev/null +++ b/contracts/treasury_manager/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +jobs: + build: + docker: + - image: rust:1.43.1 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + - run: + name: Build + command: cargo wasm --locked + - run: + name: Unit tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Integration tests + command: cargo integration-test --locked + - run: + name: Format source code + command: cargo fmt + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in source code and schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + - target/wasm32-unknown-unknown/release/.fingerprint + - target/wasm32-unknown-unknown/release/build + - target/wasm32-unknown-unknown/release/deps + key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} diff --git a/contracts/treasury_manager/Cargo.toml b/contracts/treasury_manager/Cargo.toml new file mode 100644 index 000000000..5173a55b4 --- /dev/null +++ b/contracts/treasury_manager/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "treasury_manager" +version = "0.1.0" +authors = ["Jack Swenson "] +edition = "2018" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +debug-print = ["cosmwasm-std/debug-print"] + +[dependencies] +cosmwasm-std = { version = "0.10", package = "secret-cosmwasm-std" } +cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } +cosmwasm-schema = "0.10.1" +secret-toolkit = { version = "0.2" } +shade-protocol = { version = "0.1.0", path = "../../packages/shade_protocol", features = [ + "treasury_manager", + "snip20" +]} +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +snafu = { version = "0.6.3" } +chrono = "0.4.19" diff --git a/contracts/treasury_manager/Makefile b/contracts/treasury_manager/Makefile new file mode 100644 index 000000000..2493c22f4 --- /dev/null +++ b/contracts/treasury_manager/Makefile @@ -0,0 +1,68 @@ +.PHONY: check +check: + cargo check + +.PHONY: clippy +clippy: + cargo clippy + +PHONY: test +test: unit-test + +.PHONY: unit-test +unit-test: + cargo test + +# This is a local build with debug-prints activated. Debug prints only show up +# in the local development chain (see the `start-server` command below) +# and mainnet won't accept contracts built with the feature enabled. +.PHONY: build _build +build: _build compress-wasm +_build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --features="debug-print" + +# This is a build suitable for uploading to mainnet. +# Calls to `debug_print` get removed by the compiler. +.PHONY: build-mainnet _build-mainnet +build-mainnet: _build-mainnet compress-wasm +_build-mainnet: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +# like build-mainnet, but slower and more deterministic +.PHONY: build-mainnet-reproducible +build-mainnet-reproducible: + docker run --rm -v "$$(pwd)":/contract \ + --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/contract/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + enigmampc/secret-contract-optimizer:1.0.3 + +.PHONY: compress-wasm +compress-wasm: + cp ./target/wasm32-unknown-unknown/release/*.wasm ./contract.wasm + @## The following line is not necessary, may work only on linux (extra size optimization) + @# wasm-opt -Os ./contract.wasm -o ./contract.wasm + cat ./contract.wasm | gzip -9 > ./contract.wasm.gz + +.PHONY: schema +schema: + cargo run --example schema + +# Run local development chain with four funded accounts (named a, b, c, and d) +.PHONY: start-server +start-server: # CTRL+C to stop + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1317:1317 \ + -v $$(pwd):/root/code \ + --name secretdev enigmampc/secret-network-sw-dev:v1.0.4-3 + +# This relies on running `start-server` in another console +# You can run other commands on the secretcli inside the dev image +# by using `docker exec secretdev secretcli`. +.PHONY: store-contract-local +store-contract-local: + docker exec secretdev secretcli tx compute store -y --from a --gas 1000000 /root/code/contract.wasm.gz + +.PHONY: clean +clean: + cargo clean + -rm -f ./contract.wasm ./contract.wasm.gz diff --git a/contracts/treasury_manager/README.md b/contracts/treasury_manager/README.md new file mode 100644 index 000000000..021e34011 --- /dev/null +++ b/contracts/treasury_manager/README.md @@ -0,0 +1,142 @@ +# Treasury Contract +* [Introduction](#Introduction) +* [Sections](#Sections) + * [DAO Adapter](/packages/shade_protocol/src/DAO_ADAPTER.md) + * [Init](#Init) + * [Interface](#Interface) + * Messages + * [UpdateConfig](#UpdateConfig) + * [RegisterAsset](#RegisterAsset) + * [Allocate](#Allocate) + * Queries + * [Config](#Config) + * [Assets](#Assets) + * [PendingAllowance](#PendingAllowance) +# Introduction +The treasury contract holds network funds from things such as mint commission and pending airdrop funds + +# Sections + +## Init +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|admin | HumanAddr| Admin address +|viewing_key | String | Key set on relevant SNIP-20's +|treasury | HumanAddr | treasury that is owner of funds + +## Interface + +### Messages +#### UpdateConfig +Updates the given values +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|config | Config | New contract config +##### Response +```json +{ + "update_config": { + "status": "success" + } +} +``` + +#### RegisterAsset +Registers a supported asset. The asset must be SNIP-20 compliant since [RegisterReceive](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md#RegisterReceive) is called. + +Note: Will return an error if there's an asset with that address already registered. +##### Request +|Name |Type |Description | optional | +|------------|--------|-----------------------------------------------------------------------------------------------------------------------|----------| +|contract | Contract | Type explained [here](#Contract) | no | +##### Response +```json +{ + "register_asset": { + "status": "success" + } +} +``` + +#### Allocate +Registers a supported asset. The asset must be SNIP-20 compliant since [RegisterReceive](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md#RegisterReceive) is called. + +Note: Will return an error if there's an asset with that address already registered. +##### Request +|Name |Type |Description | optional | +|------------|--------|-----------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | Desired SNIP-20 +|allocation | Allocation | Allocation data +##### Response +```json +{ + "allocate": { + "status": "success" + } +} +``` + +### Queries + +#### Config +Gets the contract's configuration variables +##### Response +```json +{ + "config": { + "config": { .. } + } +} +``` + +#### Assets +Get the list of registered assets +##### Response +```json +{ + "assets": { + "assets": ["asset address", ..], + } +} +``` + +#### Allocations +Get the allocations for a given asset + +##### Request +|Name |Type |Description | optional | +|------------|--------|-----------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | Address of desired SNIP-20 asset + +##### Response +```json +{ + "allocations": { + "allocations": [ + { + "allocation": {}, + }, + .. + ], + } +} +``` + +#### PendingAllowance +Get the pending allowance for a given asset + +##### Request +|Name |Type |Description | optional | +|------------|--------|-----------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | Address of desired SNIP-20 asset + +##### Response +```json +{ + "pending_allowance": { + "amount": "100000", + } +} +``` diff --git a/contracts/treasury_manager/src/contract.rs b/contracts/treasury_manager/src/contract.rs new file mode 100644 index 000000000..5f8bc2b85 --- /dev/null +++ b/contracts/treasury_manager/src/contract.rs @@ -0,0 +1,107 @@ +use cosmwasm_std::{ + debug_print, to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, + StdResult, StdError, Storage, +}; + +use shade_protocol::{ + adapter, + treasury_manager::{ + Config, HandleMsg, InitMsg, QueryMsg + }, +}; + +use crate::{ + handle, query, + state::{ + allocations_w, asset_list_w, config_w, self_address_w, + viewing_key_w, + }, +}; +use chrono::prelude::*; + +pub fn init( + deps: &mut Extern, + env: Env, + msg: InitMsg, +) -> StdResult { + + config_w(&mut deps.storage).save(&Config { + admin: msg.admin.unwrap_or(env.message.sender.clone()), + treasury: msg.treasury, + })?; + + viewing_key_w(&mut deps.storage).save(&msg.viewing_key)?; + self_address_w(&mut deps.storage).save(&env.contract.address)?; + asset_list_w(&mut deps.storage).save(&Vec::new())?; + + debug_print!("Contract was initialized by {}", env.message.sender); + + Ok(InitResponse { + messages: vec![], + log: vec![], + }) +} + +pub fn handle( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + match msg { + /* + HandleMsg::Receive { + sender, + from, + amount, + msg, + .. + } => handle::receive(deps, env, sender, from, amount, msg), + */ + HandleMsg::UpdateConfig { + config + } => handle::try_update_config(deps, env, config), + HandleMsg::RegisterAsset { + contract + } => handle::try_register_asset(deps, &env, &contract), + HandleMsg::Allocate { + asset, + allocation + } => handle::allocate(deps, &env, asset, allocation), + HandleMsg::Adapter(a) => match a { + adapter::SubHandleMsg::Unbond { + asset, + amount + } => handle::unbond(deps, &env, asset, amount), + adapter::SubHandleMsg::Claim { asset } => handle::claim(deps, &env, asset), + adapter::SubHandleMsg::Update { + asset + } => handle::update(deps, &env, asset), + } + } +} + +pub fn query( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + + match msg { + QueryMsg::Config {} => to_binary(&query::config(deps)?), + QueryMsg::Assets {} => to_binary(&query::assets(deps)?), + QueryMsg::Allocations { + asset + } => to_binary(&query::allocations(deps, asset)?), + QueryMsg::PendingAllowance { + asset + } => to_binary(&query::pending_allowance(deps, asset)?), + QueryMsg::Adapter(a) => match a { + adapter::SubQueryMsg::Balance { + asset + } => to_binary(&query::balance(deps, &asset)?), + adapter::SubQueryMsg::Unbonding { asset } => to_binary(&query::unbonding(deps, asset)?), + adapter::SubQueryMsg::Unbondable { asset } => to_binary(&query::unbondable(deps, asset)?), + adapter::SubQueryMsg::Claimable { asset } => to_binary(&query::claimable(deps, asset)?), + } + } + +} diff --git a/contracts/treasury_manager/src/handle.rs b/contracts/treasury_manager/src/handle.rs new file mode 100644 index 000000000..af3f11658 --- /dev/null +++ b/contracts/treasury_manager/src/handle.rs @@ -0,0 +1,448 @@ +use cosmwasm_std; +use cosmwasm_std::{ + from_binary, to_binary, Api, Binary, CosmosMsg, WasmMsg, Env, Extern, HandleResponse, HumanAddr, + Querier, StdError, StdResult, Storage, Uint128, +}; +use secret_toolkit::{ + utils::{ + Query, HandleCallback, + }, + snip20::{ + allowance_query, decrease_allowance_msg, + increase_allowance_msg, register_receive_msg, + send_msg, batch_send_from_msg, + set_viewing_key_msg, batch_send_msg, + batch::{ SendFromAction }, + }, +}; + +use shade_protocol::{ + snip20, + adapter, + treasury_manager::{ + Allocation, AllocationMeta, + AllocationType, Config, + HandleAnswer, QueryAnswer, + }, + utils::{ + asset::Contract, + generic_response::ResponseStatus + }, +}; + +use crate::{ + query, + state::{ + allocations_r, allocations_w, asset_list_r, asset_list_w, assets_r, assets_w, config_r, + config_w, viewing_key_r, + }, +}; +use chrono::prelude::*; +use std::convert::TryFrom; + +/* +pub fn receive( + deps: &mut Extern, + env: Env, + _sender: HumanAddr, + _from: HumanAddr, + amount: Uint128, + msg: Option, +) -> StdResult { + + /* TODO + * This should never receive funds, maybe should not even register receieve + * Could potentially register receive when registering an asset to forward to treasury + */ + + let config = config_r(&deps.storage).load()?; + let asset = assets_r(&deps.storage).load(env.message.sender.to_string().as_bytes())?; + + Ok(HandleResponse { + messages: vec![ + send_msg( + config.treasury, + amount, + None, + None, + None, + 1, + asset.contract.code_hash.clone(), + asset.contract.address.clone(), + )? + ], + log: vec![], + data: Some(to_binary(&HandleAnswer::Receive { + status: ResponseStatus::Success, + })?), + }) +} +*/ + +pub fn try_update_config( + deps: &mut Extern, + env: Env, + config: Config, +) -> StdResult { + let cur_config = config_r(&deps.storage).load()?; + + if env.message.sender != cur_config.admin { + return Err(StdError::unauthorized()); + } + + config_w(&mut deps.storage).save(&config)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::UpdateConfig { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn try_register_asset( + deps: &mut Extern, + env: &Env, + contract: &Contract, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + + if env.message.sender != config.admin { + return Err(StdError::unauthorized()); + } + + asset_list_w(&mut deps.storage).update(|mut list| { + list.push(contract.address.clone()); + Ok(list) + })?; + + assets_w(&mut deps.storage).save( + contract.address.to_string().as_bytes(), + &snip20::fetch_snip20(contract, &deps.querier)?, + )?; + + allocations_w(&mut deps.storage).save(contract.address.as_str().as_bytes(), &Vec::new())?; + + Ok(HandleResponse { + messages: vec![ + // Register contract in asset + register_receive_msg( + env.contract_code_hash.clone(), + None, + 256, + contract.code_hash.clone(), + contract.address.clone(), + )?, + // Set viewing key + set_viewing_key_msg( + viewing_key_r(&deps.storage).load()?, + None, + 256, + contract.code_hash.clone(), + contract.address.clone(), + )?, + ], + log: vec![], + data: Some(to_binary(&HandleAnswer::RegisterAsset { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn allocate( + deps: &mut Extern, + env: &Env, + asset: HumanAddr, + allocation: Allocation, +) -> StdResult { + + static ONE_HUNDRED_PERCENT: u128 = 10u128.pow(18); + + let config = config_r(&deps.storage).load()?; + + /* ADMIN ONLY */ + if env.message.sender != config.admin { + return Err(StdError::unauthorized()); + } + + let key = asset.as_str().as_bytes(); + + let mut apps = allocations_r(&deps.storage) + .may_load(key)? + .unwrap_or_default(); + + let stale_alloc = apps.iter().position(|a| a.contract.address == allocation.contract.address); + + match stale_alloc { + Some(i) => { apps.remove(i); } + None => { } + }; + + apps.push( + AllocationMeta { + nick: allocation.nick, + contract: allocation.contract, + amount: allocation.amount, + alloc_type: allocation.alloc_type, + balance: Uint128::zero(), + } + ); + + if (apps.iter().map(|a| { + if a.alloc_type == AllocationType::Portion { + a.amount.u128() + } else { + 0 + } + }).sum::()) > ONE_HUNDRED_PERCENT { + return Err(StdError::generic_err( + "Invalid allocation total exceeding 100%", + )); + } + + allocations_w(&mut deps.storage).save(key, &apps)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![], + data: Some(to_binary(&HandleAnswer::Allocate{ + status: ResponseStatus::Success, + })?), + }) +} + +pub fn claim( + deps: &mut Extern, + env: &Env, + asset: HumanAddr, +) -> StdResult { + + if assets_r(&deps.storage).may_load(asset.as_str().as_bytes())?.is_none() { + return Err(StdError::generic_err("Not an asset")); + } + + let mut total_claimable = Uint128::zero(); + let mut messages = vec![]; + + for alloc in allocations_r(&deps.storage).load(asset.to_string().as_bytes())? { + + let claim = adapter::claimable_query(deps, &asset.clone(), alloc.contract.clone())?; + + if claim > Uint128::zero() { + total_claimable += claim; + messages.push(adapter::claim_msg(asset.clone(), alloc.contract)?); + } + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Claim { + status: ResponseStatus::Success, + amount: total_claimable, + })?), + }) +} + +pub fn update( + deps: &mut Extern, + env: &Env, + asset: HumanAddr, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + + let full_asset = assets_r(&deps.storage).load(asset.to_string().as_bytes())?; + + let mut allocations = allocations_r(&mut deps.storage).load(asset.to_string().as_bytes())?; + + // Build metadata + let mut amount_total = Uint128::zero(); + let mut portion_total = Uint128::zero(); + + for i in 0..allocations.len() { + match allocations[i].alloc_type { + AllocationType::Amount => amount_total += allocations[i].balance, + AllocationType::Portion => { + allocations[i].balance = adapter::balance_query(deps, + &full_asset.contract.address, + allocations[i].contract.clone())?; + portion_total += allocations[i].balance; + } + }; + } + + // Batch send_from actions + let mut send_actions = vec![]; + let mut messages = vec![]; + + let mut allowance = allowance_query( + &deps.querier, + config.treasury.clone(), + env.contract.address.clone(), + viewing_key_r(&deps.storage).load()?, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )?.allowance; + + let total = portion_total + allowance; + + let mut total_unbond = Uint128::zero(); + let mut total_input = Uint128::zero(); + + for adapter in allocations.clone() { + match adapter.alloc_type { + // TODO Separate handle for amount refresh + AllocationType::Amount => { }, + AllocationType::Portion => { + + let desired_amount = adapter.amount.multiply_ratio( + total, 10u128.pow(18) + ); + + // .05 || 5% + //let REBALANCE_THRESHOLD = Uint128(5u128 * 10u128.pow(16)); + + if adapter.balance < desired_amount { + // Need to add more from allowance + let input_amount = (desired_amount - adapter.balance)?; + + if input_amount <= allowance { + total_input += input_amount; + send_actions.push( + SendFromAction { + owner: config.treasury.clone(), + recipient: adapter.contract.address, + recipient_code_hash: Some(adapter.contract.code_hash), + amount: input_amount, + msg: None, + memo: None, + } + ); + allowance = (allowance - input_amount)?; + } + else { + total_input += allowance; + // Send all allowance + send_actions.push(SendFromAction { + owner: config.treasury.clone(), + recipient: adapter.contract.address, + recipient_code_hash: Some(adapter.contract.code_hash), + amount: allowance, + msg: None, + memo: None, + }); + + allowance = Uint128::zero(); + break; + } + } + }, + }; + } + + if !send_actions.is_empty() { + messages.push( + batch_send_from_msg( + send_actions, + None, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )? + ); + } + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Update { + status: ResponseStatus::Success, + })?), + }) +} + +pub fn unbond( + deps: &mut Extern, + env: &Env, + asset: HumanAddr, + amount: Uint128, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + + let full_asset = assets_r(&deps.storage).load(asset.to_string().as_bytes())?; + + let mut allocations = allocations_r(&mut deps.storage).load(asset.to_string().as_bytes())?; + + // Build metadata + let mut amount_total = Uint128::zero(); + let mut portion_total = Uint128::zero(); + + for i in 0..allocations.len() { + match allocations[i].alloc_type { + AllocationType::Amount => amount_total += allocations[i].balance, + AllocationType::Portion => { + allocations[i].balance = adapter::balance_query(deps, + &full_asset.contract.address, + allocations[i].contract.clone())?; + portion_total += allocations[i].balance; + } + }; + } + + // Batch send_from actions + let mut messages = vec![]; + + let mut allowance = allowance_query( + &deps.querier, + config.treasury.clone(), + env.contract.address.clone(), + viewing_key_r(&deps.storage).load()?, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )?.allowance; + + let total = portion_total + allowance; + + let mut total_unbond = Uint128::zero(); + + allocations.sort_by(|a, b| a.balance.cmp(&b.balance)); + + for i in 0..allocations.len() { + match allocations[i].alloc_type { + // TODO Separate handle for amount refresh + // Or just do cycle::constant amounts + AllocationType::Amount => { }, + AllocationType::Portion => { + + let desired_amount = allocations[i].amount.multiply_ratio( + total, 10u128.pow(18) + ); + + messages.push( + adapter::unbond_msg( + asset.clone(), + amount, + allocations[i].contract.clone() + )? + ); + + }, + }; + } + + + Ok(HandleResponse { + messages, + log: vec![], + data: Some(to_binary(&adapter::HandleAnswer::Unbond { + status: ResponseStatus::Success, + amount: total_unbond + })?), + }) +} diff --git a/contracts/treasury_manager/src/lib.rs b/contracts/treasury_manager/src/lib.rs new file mode 100644 index 000000000..5ed186c7b --- /dev/null +++ b/contracts/treasury_manager/src/lib.rs @@ -0,0 +1,44 @@ +pub mod contract; +pub mod handle; +pub mod query; +pub mod state; + +#[cfg(test)] +mod test; + +#[cfg(target_arch = "wasm32")] +mod wasm { + use super::contract; + use cosmwasm_std::{ + do_handle, do_init, do_query, ExternalApi, ExternalQuerier, ExternalStorage, + }; + + #[no_mangle] + extern "C" fn init(env_ptr: u32, msg_ptr: u32) -> u32 { + do_init( + &contract::init::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn handle(env_ptr: u32, msg_ptr: u32) -> u32 { + do_handle( + &contract::handle::, + env_ptr, + msg_ptr, + ) + } + + #[no_mangle] + extern "C" fn query(msg_ptr: u32) -> u32 { + do_query( + &contract::query::, + msg_ptr, + ) + } + + // Other C externs like cosmwasm_vm_version_1, allocate, deallocate are available + // automatically because we `use cosmwasm_std`. +} diff --git a/contracts/treasury_manager/src/query.rs b/contracts/treasury_manager/src/query.rs new file mode 100644 index 000000000..4e4a8ef81 --- /dev/null +++ b/contracts/treasury_manager/src/query.rs @@ -0,0 +1,177 @@ +use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage, Uint128}; +use secret_toolkit::{ + snip20::allowance_query, + utils::Query, +}; +use shade_protocol::{ + snip20, + treasury_manager, + adapter, + utils::asset::Contract, +}; + +use crate::state::{ + allocations_r, asset_list_r, assets_r, config_r, self_address_r, + viewing_key_r, +}; + +pub fn config( + deps: &Extern, +) -> StdResult { + Ok(treasury_manager::QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + }) +} + + + +pub fn pending_allowance( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let config = config_r(&deps.storage).load()?; + let full_asset = match assets_r(&deps.storage).may_load(asset.as_str().as_bytes())? { + Some(a) => a, + None => { + return Err(StdError::generic_err("")); + } + }; + + let allowance = allowance_query( + &deps.querier, + config.treasury, + self_address_r(&deps.storage).load()?, + viewing_key_r(&deps.storage).load()?, + 1, + full_asset.contract.code_hash.clone(), + full_asset.contract.address.clone(), + )?; + + Ok(treasury_manager::QueryAnswer::PendingAllowance { + amount: allowance.allowance + }) +} + +pub fn balance( + deps: &Extern, + asset: &HumanAddr, +) -> StdResult { + + if let Some(full_asset) = assets_r(&deps.storage).may_load(asset.to_string().as_bytes())? { + + let allocs = allocations_r(&deps.storage).load(asset.as_str().as_bytes())?; + + let mut total_balance = Uint128::zero(); + + for alloc in allocs { + total_balance += adapter::balance_query(&deps, + &asset, + alloc.contract.clone(), + )?; + } + + return Ok(adapter::QueryAnswer::Balance { + amount: total_balance, + }); + } + + Err(StdError::generic_err("Not a registered asset")) +} + +pub fn assets( + deps: &Extern, +) -> StdResult { + + Ok(treasury_manager::QueryAnswer::Assets { + assets: asset_list_r(&deps.storage).load()?, + }) +} + +pub fn allocations( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + Ok(treasury_manager::QueryAnswer::Allocations { + allocations: match allocations_r(&deps.storage).may_load(asset.to_string().as_bytes())? { + None => vec![], + Some(a) => a, + }, + }) +} + +pub fn claimable( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let allocations = match allocations_r(&deps.storage).may_load(asset.to_string().as_bytes())? { + Some(a) => a, + None => { return Err(StdError::generic_err("Not an asset")); } + }; + + let mut claimable = Uint128::zero(); + + for alloc in allocations { + claimable += adapter::claimable_query(&deps, + &asset, + alloc.contract.clone(), + )?; + } + + Ok(adapter::QueryAnswer::Claimable { + amount: claimable, + }) +} + +pub fn unbonding( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let allocations = match allocations_r(&deps.storage).may_load(asset.to_string().as_bytes())? { + Some(a) => a, + None => { return Err(StdError::generic_err("Not an asset")); } + }; + + let mut unbonding = Uint128::zero(); + + for alloc in allocations { + unbonding += adapter::unbonding_query(&deps, + &asset, + alloc.contract.clone(), + )?; + } + + Ok(adapter::QueryAnswer::Unbonding { + amount: unbonding, + }) +} + +/*NOTE Could be a situation where can_unbond returns true + * but only partial balance available for unbond resulting + * in stalled treasury trying to unbond more than is available + */ +pub fn unbondable( + deps: &Extern, + asset: HumanAddr, +) -> StdResult { + + let allocations = match allocations_r(&deps.storage).may_load(asset.to_string().as_bytes())? { + Some(a) => a, + None => { return Err(StdError::generic_err("Not an asset")); } + }; + + let mut unbondable = Uint128::zero(); + + for alloc in allocations { + // return true if any + unbondable += adapter::unbondable_query(&deps, + &asset, alloc.contract.clone())?; + } + + Ok(adapter::QueryAnswer::Unbondable { + amount: unbondable, + }) +} diff --git a/contracts/treasury_manager/src/state.rs b/contracts/treasury_manager/src/state.rs new file mode 100644 index 000000000..116d195db --- /dev/null +++ b/contracts/treasury_manager/src/state.rs @@ -0,0 +1,66 @@ +use cosmwasm_std::{HumanAddr, Storage, Uint128}; +use cosmwasm_storage::{ + bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, + Singleton, +}; +use shade_protocol::{ + snip20::Snip20Asset, + treasury_manager, +}; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static ASSETS: &[u8] = b"assets"; +pub static ASSET_LIST: &[u8] = b"asset_list"; +pub static VIEWING_KEY: &[u8] = b"viewing_key"; +pub static SELF_ADDRESS: &[u8] = b"self_address"; +pub static ALLOCATIONS: &[u8] = b"allocations"; +//pub static ALLOWANCE_REFRESH: &[u8] = b"allowance_refresh"; +//pub static REWARDS: &[u8] = b"rewards_tracking"; + +pub fn config_w(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn asset_list_r(storage: &S) -> ReadonlySingleton> { + singleton_read(storage, ASSET_LIST) +} + +pub fn asset_list_w(storage: &mut S) -> Singleton> { + singleton(storage, ASSET_LIST) +} + +pub fn assets_r(storage: &S) -> ReadonlyBucket { + bucket_read(ASSETS, storage) +} + +pub fn assets_w(storage: &mut S) -> Bucket { + bucket(ASSETS, storage) +} + +pub fn viewing_key_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, VIEWING_KEY) +} + +pub fn viewing_key_w(storage: &mut S) -> Singleton { + singleton(storage, VIEWING_KEY) +} + +pub fn self_address_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, SELF_ADDRESS) +} + +pub fn self_address_w(storage: &mut S) -> Singleton { + singleton(storage, SELF_ADDRESS) +} + +pub fn allocations_r(storage: &S) -> ReadonlyBucket> { + bucket_read(ALLOCATIONS, storage) +} + +pub fn allocations_w(storage: &mut S) -> Bucket> { + bucket(ALLOCATIONS, storage) +} diff --git a/contracts/treasury_manager/src/test.rs b/contracts/treasury_manager/src/test.rs new file mode 100644 index 000000000..879d17eb4 --- /dev/null +++ b/contracts/treasury_manager/src/test.rs @@ -0,0 +1,38 @@ +#[cfg(test)] +pub mod tests { + /* + use cosmwasm_std::{ + testing::{ + mock_dependencies, mock_env, MockStorage, MockApi, MockQuerier + }, + HumanAddr, coins, Extern, + }; + use shade_protocol::{ + treasury::InitMsg, + }; + + use crate::{ + contract::init, + }; + + fn create_contract(address: &str, code_hash: &str) -> Contract { + let env = mock_env(address.to_string(), &[]); + return Contract{ + address: env.message.sender, + code_hash: code_hash.to_string() + } + } + + fn dummy_init(admin: String, viewing_key: String) -> Extern { + let mut deps = mock_dependencies(20, &[]); + let msg = InitMsg { + admin: Option::from(HumanAddr(admin.clone())), + viewing_key, + }; + let env = mock_env(admin, &coins(1000, "earth")); + let _res = init(&mut deps, env, msg).unwrap(); + + return deps + } + */ +} diff --git a/dao.drawio b/dao.drawio new file mode 100644 index 000000000..09709c130 --- /dev/null +++ b/dao.drawio @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deploy-mint-local.py b/deploy-mint-local.py index 2357eb048..3948ccb36 100755 --- a/deploy-mint-local.py +++ b/deploy-mint-local.py @@ -244,6 +244,7 @@ # mints 'shade_mint': shade_mint.address, 'silk_mint': silk_mint.address, + 'mint_router': mint_router.address, 'oracle': oracle.address, 'band': mock_band.address, diff --git a/makefile b/makefile index 881862ce3..b5944e5e9 100755 --- a/makefile +++ b/makefile @@ -2,8 +2,8 @@ contracts_dir=contracts compiled_dir=compiled checksum_dir=${compiled_dir}/checksum -build-release=RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --locked -build-debug=RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --locked --features="debug-print" +build-release=RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown +build-debug=RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --features="debug-print" # args (no extensions): wasm_name, contract_dir_name define opt_and_compress = @@ -15,7 +15,8 @@ endef CONTRACTS = \ airdrop governance shd_staking mint mint_router \ - treasury oracle initializer scrt_staking snip20 \ + treasury treasury_manager scrt_staking rewards_emission \ + oracle initializer snip20 \ mock_band mock_secretswap_pair mock_sienna_pair debug: setup @@ -26,6 +27,8 @@ release: setup (cd ${contracts_dir}; ${build-release}) @$(MAKE) compress_all +dao: treasury treasury_manager scrt_staking rewards_emission + compress_all: setup @$(MAKE) $(addprefix compress-,$(CONTRACTS)) @@ -46,11 +49,12 @@ snip20: setup (cd ${contracts_dir}/snip20; ${build-release}) @$(MAKE) $(addprefix compress-,snip20) + test: @$(MAKE) $(addprefix test-,$(CONTRACTS)) test-%: - (cd ${contracts_dir}/$*; cargo unit-test) + (cd ${contracts_dir}/$*; cargo test) shd_staking: setup (cd ${contracts_dir}/shd_staking; ${build-release}) @@ -68,6 +72,7 @@ clippy: cargo clippy clean: + find . -name "Cargo.lock" -delete rm -r $(compiled_dir) format: diff --git a/packages/shade_protocol/Cargo.toml b/packages/shade_protocol/Cargo.toml index 0320aea75..6f9672d46 100644 --- a/packages/shade_protocol/Cargo.toml +++ b/packages/shade_protocol/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"] [features] default = ["utils"] + # Templates dex = ["utils", "math", "snip20", "mint", "secretswap", "sienna", "band"] band = [] @@ -24,6 +25,7 @@ utils = [] errors = [] flexible_msg = [] math = [] + storage = ["cosmwasm-storage/iterator"] storage_plus = ["dep:secret-storage-plus"] @@ -34,9 +36,12 @@ governance = ["utils"] mint = ["utils", "snip20"] mint_router = ["utils", "snip20"] oracle = ["snip20"] -scrt_staking = ["utils"] +scrt_staking= ["utils", "adapter", "treasury"] +treasury = ["utils", "adapter", "snip20"] +treasury_manager = ["adapter"] +rewards_emission = ["adapter"] +adapter = [] shd_staking = ["utils"] -treasury = ["utils"] # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces @@ -53,9 +58,10 @@ schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } snafu = { version = "0.6.3" } # Needed for transactions +chrono = "0.4.19" + query-authentication = { git = "https://github.com/securesecrets/query-authentication", tag = "v1.2.0", optional = true } # Used by airdrop remain = { version = "0.2.2", optional = true } # storage plus implementation secret-storage-plus = { git = "https://github.com/securesecrets/secret-storage-plus", tag = "v1.0.0", optional = true } - diff --git a/packages/shade_protocol/src/DAO_ADAPTER.md b/packages/shade_protocol/src/DAO_ADAPTER.md new file mode 100644 index 000000000..d2927b86b --- /dev/null +++ b/packages/shade_protocol/src/DAO_ADAPTER.md @@ -0,0 +1,153 @@ +# DAO Adapter Interface +* [Introduction](#Introduction) +* [Sections](#Sections) + * [Interface](#Interface) + * Messages + * [Unbond](#Unbond) + * [Claim](#Claim) + * [Update](#Update) + * Queries + * [Balance](#Balance) + * [Unbonding](#Unbonding) + * [Claimable](#Claimable) + * [Unbondable](#Unbondable) + +# Introduction +This is an interface for dapps to follow to integrate with the DAO, to receive funding fromthe treasury and later unbond those funds back to treasury when needed. +NOTE: Because of how the contract implements this, all messages will be enclosed as: +``` +{ + "adapter": { + + } +} +``` + +# Sections + +### Messages +#### Unbond +Begin unbonding of a given amount from a given asset + +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | SNIP-20 asset to unbond + +##### Response +```json +{ + "unbond": { + "amount": "100" + "status": "success" + } +} +``` + +#### Claim +Claim a given amount from completed unbonding of a given asset + +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | SNIP-20 asset to unbond + +##### Response +```json +{ + "claim": { + "amount": "100" + "status": "success" + } +} +``` + +#### Update +Update a given asset on the adapter, to perform regular maintenance tasks if needed +Examples: + - `scrt_staking` - Claim rewards and restake + - `treasury` - Rebalance funds + +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | SNIP-20 asset to unbond + +##### Response +```json +{ + "update": { + "status": "success" + } +} +``` + +### Queries + +#### Balance +Get the balance of a given asset, Error if unrecognized + +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | SNIP-20 asset to query + +##### Response +```json +{ + "balance": { + "amount": "100000", + } +} +``` + +#### Unbonding +Get the current unbonding amount of a given asset, Error if unrecognized + +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | SNIP-20 asset to query + +##### Response +```json +{ + "unbonding": { + "amount": "100000", + } +} +``` + +#### Claimable +Get the current claimable amount of a given asset, Error if unrecognized + +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | SNIP-20 asset to query + +##### Response +```json +{ + "claimable": { + "amount": "100000", + } +} +``` + +#### Unbondable +Get the current unbondable amount of a given asset, Error if unrecognized + +##### Request +|Name |Type |Description | optional | +|----------|----------|-------------------------------------------------------------------------------------------------------------------|----------| +|asset | HumanAddr | SNIP-20 asset to query + +##### Response +```json +{ + "unbondable": { + "amount": "100000", + } +} +``` diff --git a/packages/shade_protocol/src/adapter.rs b/packages/shade_protocol/src/adapter.rs new file mode 100644 index 000000000..47f5e1777 --- /dev/null +++ b/packages/shade_protocol/src/adapter.rs @@ -0,0 +1,223 @@ +use crate::utils::{asset::Contract, generic_response::ResponseStatus}; +use cosmwasm_std::{Binary, Decimal, Delegation, HumanAddr, Uint128, Validator, StdResult, StdError, Extern, Api, Querier, Storage, CosmosMsg}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +use serde::{Deserialize, Serialize}; + +/* +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum BondStatus { + Active, + Unbonding, + UnbondComplete, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Bond { + pub amount: Uint128, + pub token: Contract, + pub address: HumanAddr, + pub status: BondStatus, +} +*/ + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SubHandleMsg { + // Begin unbonding amount + Unbond { asset: HumanAddr, amount: Uint128 }, + Claim { asset: HumanAddr }, + // Maintenance trigger e.g. claim rewards and restake + Update { asset: HumanAddr }, +} + +impl HandleCallback for SubHandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + Adapter(SubHandleMsg), +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + Init { + status: ResponseStatus, + address: HumanAddr, + }, + Unbond { + status: ResponseStatus, + amount: Uint128, + }, + Claim { + status: ResponseStatus, + amount: Uint128, + }, + Update { + status: ResponseStatus, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SubQueryMsg { + Balance { asset: HumanAddr }, + Unbonding { asset: HumanAddr }, + Claimable { asset: HumanAddr }, + Unbondable { asset: HumanAddr }, + /* TODO + * - LP pool assets + * Ratio { asset0: HumanAddr, asset1: HumanAddr }, + * - things like unbond period + * Metadata { asset: HumanAddr }, + * - How much is available to unbond + * Unbondable { asset: HumanAddr }, + */ +} + +/* +impl Query for SubQueryMsg { + const BLOCK_SIZE: usize = 256; +} +*/ + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + Adapter(SubQueryMsg), +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + Balance { amount: Uint128 }, + Unbonding { amount: Uint128 }, + Claimable { amount: Uint128 }, + Unbondable { amount: Uint128 }, +} + +pub fn claimable_query( + deps: &Extern, + asset: &HumanAddr, + adapter: Contract, +) -> StdResult { + + match (QueryMsg::Adapter(SubQueryMsg::Claimable { + asset: asset.clone(), + }).query(&deps.querier, adapter.code_hash, adapter.address.clone())?) { + QueryAnswer::Claimable { amount } => Ok(amount), + _ => Err(StdError::generic_err( + format!("Failed to query adapter claimable from {}", adapter.address) + )) + } +} + +pub fn unbonding_query( + deps: &Extern, + asset: &HumanAddr, + adapter: Contract, +) -> StdResult { + + match (QueryMsg::Adapter(SubQueryMsg::Unbonding { + asset: asset.clone(), + }).query(&deps.querier, adapter.code_hash, adapter.address.clone())?) { + QueryAnswer::Unbonding { amount } => Ok(amount), + _ => Err(StdError::generic_err( + format!("Failed to query adapter unbonding from {}", adapter.address) + )) + } +} + +pub fn unbondable_query( + deps: &Extern, + asset: &HumanAddr, + adapter: Contract, +) -> StdResult { + + match (QueryMsg::Adapter(SubQueryMsg::Unbondable { + asset: asset.clone(), + }).query(&deps.querier, adapter.code_hash, adapter.address.clone())?) { + QueryAnswer::Unbondable { amount } => Ok(amount), + _ => Err(StdError::generic_err( + format!("Failed to query adapter unbondable from {}", adapter.address) + )) + } +} + +pub fn balance_query( + deps: &Extern, + asset: &HumanAddr, + adapter: Contract, +) -> StdResult { + + match (QueryMsg::Adapter( + SubQueryMsg::Balance { + asset: asset.clone(), + } + ).query(&deps.querier, adapter.code_hash, adapter.address.clone())?) { + QueryAnswer::Balance { amount } => Ok(amount), + _ => Err(StdError::generic_err( + format!("Failed to query adapter balance from {}", adapter.address) + )) + } +} + +pub fn claim_msg( + asset: HumanAddr, + adapter: Contract, +) -> StdResult { + Ok(HandleMsg::Adapter( + SubHandleMsg::Claim { + asset + }).to_cosmos_msg( + adapter.code_hash, + adapter.address, + None + )? + ) +} + +pub fn unbond_msg( + asset: HumanAddr, + amount: Uint128, + adapter: Contract, +) -> StdResult { + Ok(HandleMsg::Adapter( + SubHandleMsg::Unbond{ + asset, + amount + }).to_cosmos_msg( + adapter.code_hash, + adapter.address, + None + )? + ) +} + +pub fn update_msg( + asset: HumanAddr, + adapter: Contract, +) -> StdResult { + Ok(HandleMsg::Adapter( + SubHandleMsg::Update { + asset + }).to_cosmos_msg( + adapter.code_hash, + adapter.address, + None + )? + ) +} diff --git a/packages/shade_protocol/src/band.rs b/packages/shade_protocol/src/band.rs index f92fc0508..d94445702 100644 --- a/packages/shade_protocol/src/band.rs +++ b/packages/shade_protocol/src/band.rs @@ -1,6 +1,5 @@ use crate::utils::asset::Contract; -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{Api, Extern, Querier, StdResult, Storage}; +use cosmwasm_std::{Api, Extern, Querier, StdResult, Storage, Uint128,}; use schemars::JsonSchema; use secret_toolkit::utils::{InitCallback, Query}; use serde::{Deserialize, Serialize}; diff --git a/packages/shade_protocol/src/dex.rs b/packages/shade_protocol/src/dex.rs index d19867ee7..e108fc4f1 100644 --- a/packages/shade_protocol/src/dex.rs +++ b/packages/shade_protocol/src/dex.rs @@ -1,21 +1,21 @@ use crate::{ - band, - //shadeswap, - mint, - secretswap, - sienna, - snip20::Snip20Asset, utils::{ - asset::Contract, price::{normalize_price, translate_price}, + asset::Contract, }, + snip20::Snip20Asset, + mint, + secretswap, + sienna, + band, + //shadeswap, }; +use cosmwasm_std::{StdResult, Extern, Querier, Api, Storage, StdError}; use cosmwasm_std; -use cosmwasm_std::{Api, Extern, Querier, StdError, StdResult, Storage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cosmwasm_math_compat::{Uint128, Uint512}; +use cosmwasm_math_compat::{Uint512, Uint128}; use std::convert::TryFrom; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -36,8 +36,9 @@ pub struct TradingPair { /* give_amount into give_pool * returns how much to be received from take_pool */ -pub fn pool_take_amount(give_amount: Uint128, give_pool: Uint128, take_pool: Uint128) -> Uint128 { - take_pool - ((give_pool * take_pool) / (give_pool + give_amount)) + +pub fn pool_take_amount(give_amount: cosmwasm_std::Uint128, give_pool: cosmwasm_std::Uint128, take_pool: cosmwasm_std::Uint128) -> cosmwasm_std::Uint128 { + cosmwasm_std::Uint128(take_pool.u128() - give_pool.u128() * take_pool.u128() / (give_pool + give_amount).u128()) } pub fn aggregate_price( @@ -45,7 +46,8 @@ pub fn aggregate_price( pairs: Vec, sscrt: Contract, band: Contract, -) -> StdResult { +) -> StdResult { + // indices will align with let mut amounts_per_scrt = vec![]; let mut pool_sizes: Vec = vec![]; @@ -53,42 +55,50 @@ pub fn aggregate_price( for pair in pairs.clone() { match &pair.dex { Dex::SecretSwap => { - amounts_per_scrt.push(Uint512::from(normalize_price( - secretswap::amount_per_scrt(&deps, pair.clone(), sscrt.clone())?, - pair.asset.token_info.decimals, - ))); - pool_sizes.push(Uint512::from(secretswap::pool_cp(&deps, pair)?)); - } + amounts_per_scrt.push( + Uint512::from(normalize_price( + secretswap::amount_per_scrt(&deps, pair.clone(), sscrt.clone())?, + pair.asset.token_info.decimals + ).u128()) + ); + pool_sizes.push(Uint512::from(secretswap::pool_cp(&deps, pair)?.u128())); + }, Dex::SiennaSwap => { - amounts_per_scrt.push(Uint512::from(normalize_price( - sienna::amount_per_scrt(&deps, pair.clone(), sscrt.clone())?, - pair.asset.token_info.decimals, - ))); - pool_sizes.push(Uint512::from(sienna::pool_cp(&deps, pair)?)); - } /* - ShadeSwap => { - prices.push(shadeswap::price(&deps, pair.clone(), sscrt.clone(), band.clone())?); - pool_sizes.push(shadeswap::pool_size(&deps, pair)?); - return Err(StdErr::generic_err("ShadeSwap Unavailable")); - }, - */ + amounts_per_scrt.push( + Uint512::from(normalize_price( + sienna::amount_per_scrt(&deps, pair.clone(), sscrt.clone())?, + pair.asset.token_info.decimals + ).u128()) + ); + pool_sizes.push(Uint512::from(sienna::pool_cp(&deps, pair)?.u128())); + }, + /* + ShadeSwap => { + prices.push(shadeswap::price(&deps, pair.clone(), sscrt.clone(), band.clone())?); + pool_sizes.push(shadeswap::pool_size(&deps, pair)?); + return Err(StdErr::generic_err("ShadeSwap Unavailable")); + }, + */ } } let mut combined_cp: Uint512 = pool_sizes.iter().sum(); - let weighted_sum: Uint512 = amounts_per_scrt - .into_iter() - .zip(pool_sizes.into_iter()) - .map(|(amount, pool_size)| amount * pool_size / combined_cp) - .sum(); + let weighted_sum: Uint512 = amounts_per_scrt.into_iter().zip(pool_sizes.into_iter()) + .map(|(a, s)| a * s / combined_cp).sum(); - // Translate price from SHD/SCRT -> SHD/USD + // Translate price from SHD/SCRT -> SHD/USD // And normalize to * 10^18 let price = translate_price( - band::reference_data(deps, "SCRT".to_string(), "USD".to_string(), band)?.rate, - Uint128::try_from(weighted_sum)?, - ); + band::reference_data(deps, + "SCRT".to_string(), + "USD".to_string(), + band + )?.rate, + cosmwasm_std::Uint128( + Uint128::try_from(weighted_sum)?.u128() + ) + ); Ok(price) } @@ -98,7 +108,8 @@ pub fn best_price( pairs: Vec, sscrt: Contract, band: Contract, -) -> StdResult<(Uint128, TradingPair)> { +) -> StdResult<(cosmwasm_std::Uint128, TradingPair)> { + // indices will align with let mut results = vec![]; @@ -141,7 +152,8 @@ pub fn price( pair: TradingPair, sscrt: Contract, band: Contract, -) -> StdResult { +) -> StdResult { + match pair.clone().dex { Dex::SecretSwap => Ok(secretswap::price( &deps, diff --git a/packages/shade_protocol/src/lib.rs b/packages/shade_protocol/src/lib.rs index 55b31bfbc..93e2f0fe5 100644 --- a/packages/shade_protocol/src/lib.rs +++ b/packages/shade_protocol/src/lib.rs @@ -43,3 +43,12 @@ pub mod shd_staking; #[cfg(feature = "treasury")] pub mod treasury; + +#[cfg(feature = "adapter")] +pub mod adapter; + +#[cfg(feature = "treasury_manager")] +pub mod treasury_manager; + +#[cfg(feature = "rewards_emission")] +pub mod rewards_emission; diff --git a/packages/shade_protocol/src/rewards_emission.rs b/packages/shade_protocol/src/rewards_emission.rs new file mode 100644 index 000000000..5f8578b7e --- /dev/null +++ b/packages/shade_protocol/src/rewards_emission.rs @@ -0,0 +1,107 @@ +use crate::{ + adapter, + utils::{ + asset::Contract, + generic_response::ResponseStatus + }, +}; +use cosmwasm_std::{Binary, Decimal, Delegation, HumanAddr, Uint128, Validator}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Reward { + pub asset: HumanAddr, + pub amount: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Config { + pub admins: Vec, + pub treasury: HumanAddr, + pub asset: Contract, + pub distributor: HumanAddr, + pub rewards: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct InitMsg { + pub config: Config, + pub viewing_key: String, +} + +impl InitCallback for InitMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + Receive { + sender: HumanAddr, + from: HumanAddr, + amount: Uint128, + memo: Option, + msg: Option, + }, + RefillRewards { + rewards: Vec, + }, + UpdateConfig { + config: Config, + }, + RegisterAsset { + asset: Contract, + }, + Adapter(adapter::SubHandleMsg), +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + Init { + status: ResponseStatus, + address: HumanAddr, + }, + UpdateConfig { + status: ResponseStatus, + }, + Receive { + status: ResponseStatus, + }, + RegisterAsset { + status: ResponseStatus, + }, + RefillRewards { + status: ResponseStatus, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + Config {}, + PendingAllowance { + asset: HumanAddr, + }, + Adapter(adapter::SubQueryMsg), +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + Config { config: Config }, + PendingAllowance { amount: Uint128 }, +} diff --git a/packages/shade_protocol/src/scrt_staking.rs b/packages/shade_protocol/src/scrt_staking.rs index 7dc6a8afd..6f5af1a51 100644 --- a/packages/shade_protocol/src/scrt_staking.rs +++ b/packages/shade_protocol/src/scrt_staking.rs @@ -1,6 +1,12 @@ -use crate::utils::{asset::Contract, generic_response::ResponseStatus}; -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{Binary, Decimal, Delegation, HumanAddr, Validator}; +use crate::{ + adapter, + utils::{ + asset::Contract, + generic_response::ResponseStatus + }, +}; +use cosmwasm_std::{Binary, Decimal, Delegation, HumanAddr, Uint128, Validator}; + use schemars::JsonSchema; use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; use serde::{Deserialize, Serialize}; @@ -39,9 +45,6 @@ impl InitCallback for InitMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleMsg { - UpdateConfig { - admin: Option, - }, Receive { sender: HumanAddr, from: HumanAddr, @@ -49,17 +52,10 @@ pub enum HandleMsg { memo: Option, msg: Option, }, - // Begin unbonding amount - Unbond { - validator: HumanAddr, - }, - //TODO: switch to this interface for standardization - //Claim { amount: Uint128 }, - - // Claim all pending rewards & completed unbondings - Claim { - validator: HumanAddr, + UpdateConfig { + config: Config, }, + Adapter(adapter::SubHandleMsg), } impl HandleCallback for HandleMsg { @@ -80,24 +76,23 @@ pub enum HandleAnswer { status: ResponseStatus, validator: Validator, }, + /* Claim { status: ResponseStatus, }, Unbond { status: ResponseStatus, - delegation: Delegation, + delegations: Vec, }, + */ } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - GetConfig {}, - //TODO: find a way to query this and return - //Unbondings {}, + Config {}, Delegations {}, - //Delegation { validator: HumanAddr }, - Rewards {}, + Adapter(adapter::SubQueryMsg), } impl Query for QueryMsg { @@ -108,5 +103,5 @@ impl Query for QueryMsg { #[serde(rename_all = "snake_case")] pub enum QueryAnswer { Config { config: Config }, - Balance { amount: Uint128 }, + //Balance { amount: Uint128 }, } diff --git a/packages/shade_protocol/src/secretswap.rs b/packages/shade_protocol/src/secretswap.rs index d11b95f26..642b26591 100644 --- a/packages/shade_protocol/src/secretswap.rs +++ b/packages/shade_protocol/src/secretswap.rs @@ -1,16 +1,18 @@ use crate::{ - band, dex, mint, utils::{ asset::Contract, price::{normalize_price, translate_price}, }, + mint, + dex, + band, }; -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage}; +use cosmwasm_std::{Uint128, HumanAddr, StdResult, StdError, Extern, Querier, Api, Storage}; use schemars::JsonSchema; use secret_toolkit::utils::Query; use serde::{Deserialize, Serialize}; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct Token { @@ -100,6 +102,7 @@ pub fn price( sscrt: Contract, band: Contract, ) -> StdResult { + let scrt_result = band::reference_data(deps, "SCRT".to_string(), "USD".to_string(), band)?; // SCRT-USD / SCRT-symbol @@ -117,9 +120,10 @@ pub fn amount_per_scrt( pair: dex::TradingPair, sscrt: Contract, ) -> StdResult { + let response: SimulationResponse = PairQuery::Simulation { offer_asset: Asset { - amount: Uint128::new(1_000_000), // 1 sSCRT (6 decimals) + amount: Uint128(1_000_000), // 1 sSCRT (6 decimals) info: AssetInfo { token: Token { contract_addr: sscrt.address, @@ -149,5 +153,5 @@ pub fn pool_cp( )?; // Constant Product - Ok(pool.assets[0].amount * pool.assets[1].amount) + Ok(Uint128(pool.assets[0].amount.u128() * pool.assets[1].amount.u128())) } diff --git a/packages/shade_protocol/src/sienna.rs b/packages/shade_protocol/src/sienna.rs index f365676fa..a9657d3f8 100644 --- a/packages/shade_protocol/src/sienna.rs +++ b/packages/shade_protocol/src/sienna.rs @@ -1,12 +1,17 @@ use crate::{ - band, dex, utils::{ asset::Contract, price::{normalize_price, translate_price}, }, + dex, + band, }; -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{Api, Extern, HumanAddr, Querier, StdError, StdResult, Storage}; +use cosmwasm_std::{ + HumanAddr, Uint128, + StdResult, StdError, + Extern, Querier, Api, Storage, +}; + use schemars::JsonSchema; use secret_toolkit::utils::Query; use serde::{Deserialize, Serialize}; @@ -117,12 +122,11 @@ pub fn price( let scrt_result = band::reference_data(deps, "SCRT".to_string(), "USD".to_string(), band)?; // SCRT-USD / SCRT-symbol - Ok(translate_price( - scrt_result.rate, - normalize_price( - amount_per_scrt(deps, pair.clone(), sscrt)?, - pair.asset.token_info.decimals, - ), + Ok(translate_price(scrt_result.rate, + normalize_price( + amount_per_scrt(deps, pair.clone(), sscrt)?, + pair.asset.token_info.decimals + ) )) } @@ -133,11 +137,11 @@ pub fn amount_per_scrt( ) -> StdResult { let response: SimulationResponse = PairQuery::SwapSimulation { offer: TokenTypeAmount { - amount: Uint128::new(1_000_000), // 1 sSCRT (6 decimals) + amount: Uint128(1_000_000), // 1 sSCRT (6 decimals) token: TokenType::CustomToken { contract_addr: sscrt.address, token_code_hash: sscrt.code_hash, - }, + } }, } .query( @@ -160,5 +164,5 @@ pub fn pool_cp( )?; // Constant Product - Ok(pair_info.pair_info.amount_0 * pair_info.pair_info.amount_1) + Ok(Uint128(pair_info.pair_info.amount_0.u128() * pair_info.pair_info.amount_1.u128())) } diff --git a/packages/shade_protocol/src/treasury.rs b/packages/shade_protocol/src/treasury.rs index 51b6f074e..97dfc9153 100644 --- a/packages/shade_protocol/src/treasury.rs +++ b/packages/shade_protocol/src/treasury.rs @@ -1,74 +1,99 @@ -use crate::utils::{asset::Contract, generic_response::ResponseStatus}; -use cosmwasm_math_compat::Uint128; -use cosmwasm_std::{Binary, HumanAddr}; +use crate::{ + adapter, + utils::{ + asset::Contract, + generic_response::ResponseStatus, + cycle::Cycle, + }, +}; + +use cosmwasm_std::{Binary, HumanAddr, Uint128, StdResult}; use schemars::JsonSchema; use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] pub struct Config { pub admin: HumanAddr, - //pub account_holders: Vec, pub sscrt: Contract, } +/* Examples: + * Constant-Portion -> Finance manager + * Constant-Amount -> Rewards, pre-set manually adjusted + * Monthly-Portion -> Rewards, self-scaling + * Monthly-Amount -> Governance grant or Committee funding + * + * Once-Portion -> Disallowed + */ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum Allocation { - // To remain liquid at all times - Reserves { - allocation: Uint128, - }, - // Won't be counted in rebalancing - Rewards { - contract: Contract, - allocation: Uint128, - }, +pub enum Allowance { // Monthly refresh, not counted in rebalance - Allowance { - address: HumanAddr, + Amount { + //nick: Option, + spender: HumanAddr, // Unlike others, this is a direct number of uTKN to allow monthly + cycle: Cycle, amount: Uint128, + last_refresh: String, }, - // SCRT/ATOM/OSMO staking - Staking { - contract: Contract, - allocation: Uint128, - }, - // SKY / Derivative Staking - Application { - contract: Contract, - allocation: Uint128, - token: HumanAddr, - }, - // Liquidity Providing - Pool { - contract: Contract, - allocation: Uint128, - secondary_asset: HumanAddr, - token: HumanAddr, + Portion { + //nick: Option, + spender: HumanAddr, + portion: Uint128, + //TODO: This needs to be omitted from the handle msg + last_refresh: String, + tolerance: Uint128, }, } -// Flag to be sent with funds #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct Flag { - pub flag: String, +pub struct Manager { + pub contract: Contract, + pub balance: Uint128, + pub desired: Uint128, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct AllowanceData { - pub spender: HumanAddr, +#[serde(rename_all = "snake_case")] +pub struct Balance { + pub token: HumanAddr, pub amount: Uint128, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Status { + Active, + Disabled, + Closed, + Transferred, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Account { + pub balances: Vec, + pub unbondings: Vec, + pub claimable: Vec, + pub status: Status, +} + +// Flag to be sent with funds +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Flag { + pub flag: String, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InitMsg { pub admin: Option, pub viewing_key: String, pub sscrt: Contract, - //pub account_holders: Option>, } impl InitCallback for InitMsg { @@ -85,12 +110,6 @@ pub enum HandleMsg { memo: Option, msg: Option, }, - OneTimeAllowance { - asset: HumanAddr, - spender: HumanAddr, - amount: Uint128, - expiration: Option, - }, UpdateConfig { config: Config, }, @@ -98,16 +117,26 @@ pub enum HandleMsg { contract: Contract, reserves: Option, }, - /* List of contracts/users given an allowance based on a percentage of the asset balance - * e.g. governance, LP, SKY - */ - RegisterAllocation { + RegisterManager { + contract: Contract, + }, + // Setup a new allowance + Allowance { asset: HumanAddr, - allocation: Allocation, + allowance: Allowance, + }, + AddAccount { + holder: HumanAddr, }, - RefreshAllowance {}, - // Trigger to re-allocate asset (all if none) - //Rebalance { asset: Option }, + CloseAccount { + holder: HumanAddr, + }, + + /* TODO: Maybe? + TransferAccount { + }, + */ + Adapter(adapter::SubHandleMsg), } impl HandleCallback for HandleMsg { @@ -121,25 +150,14 @@ pub enum HandleAnswer { status: ResponseStatus, address: HumanAddr, }, - UpdateConfig { - status: ResponseStatus, - }, - Receive { - status: ResponseStatus, - }, - RegisterAsset { - status: ResponseStatus, - }, - RegisterApp { - status: ResponseStatus, - }, - RefreshAllowance { - status: ResponseStatus, - }, - OneTimeAllowance { - status: ResponseStatus, - }, - //Rebalance { status: ResponseStatus }, + UpdateConfig { status: ResponseStatus }, + Receive { status: ResponseStatus }, + RegisterAsset { status: ResponseStatus }, + Allowance { status: ResponseStatus }, + AddAccount { status: ResponseStatus }, + RemoveAccount { status: ResponseStatus }, + Rebalance { status: ResponseStatus }, + Unbond { status: ResponseStatus }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -147,17 +165,18 @@ pub enum HandleAnswer { pub enum QueryMsg { Config {}, Assets {}, - Balance { - asset: HumanAddr, - }, - Allocations { - asset: HumanAddr, - }, - Allowances { + // List of recurring allowances configured + Allowances { asset: HumanAddr }, + // List of actual current amounts + Allowance { asset: HumanAddr, spender: HumanAddr, }, - LastAllowanceRefresh {}, + Accounts { }, + Account { + holder: HumanAddr, + }, + Adapter(adapter::SubQueryMsg), } impl Query for QueryMsg { @@ -169,8 +188,9 @@ impl Query for QueryMsg { pub enum QueryAnswer { Config { config: Config }, Assets { assets: Vec }, - Allocations { allocations: Vec }, - Balance { amount: Uint128 }, - Allowances { allowances: Vec }, - LastAllowanceRefresh { datetime: String }, + Allowances { allowances: Vec }, + CurrentAllowances { allowances: Vec }, + Allowance { allowance: Uint128 }, + Accounts { accounts: Vec }, + Account { account: Account, }, } diff --git a/packages/shade_protocol/src/treasury_manager.rs b/packages/shade_protocol/src/treasury_manager.rs new file mode 100644 index 000000000..2c9778c9b --- /dev/null +++ b/packages/shade_protocol/src/treasury_manager.rs @@ -0,0 +1,118 @@ +use crate::{ + adapter, + utils::{ + asset::Contract, + generic_response::ResponseStatus, + } +}; +use cosmwasm_std::{Binary, HumanAddr, Uint128}; +use schemars::JsonSchema; +use secret_toolkit::utils::{HandleCallback, InitCallback, Query}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Config { + pub admin: HumanAddr, + pub treasury: HumanAddr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Allocation { + pub nick: Option, + pub contract: Contract, + pub alloc_type: AllocationType, + pub amount: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AllocationType { + // amount becomes percent * 10^18 + Portion, + Amount, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct AllocationMeta { + pub nick: Option, + pub contract: Contract, + pub amount: Uint128, + pub alloc_type: AllocationType, + pub balance: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitMsg { + pub admin: Option, + pub viewing_key: String, + pub treasury: HumanAddr, +} + +impl InitCallback for InitMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + /* + Receive { + sender: HumanAddr, + from: HumanAddr, + amount: Uint128, + memo: Option, + msg: Option, + }, + */ + UpdateConfig { config: Config }, + RegisterAsset { contract: Contract }, + Allocate { + asset: HumanAddr, + allocation: Allocation, + }, + Adapter(adapter::SubHandleMsg), +} + +impl HandleCallback for HandleMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleAnswer { + Init { + status: ResponseStatus, + address: HumanAddr, + }, + Receive { status: ResponseStatus }, + UpdateConfig { status: ResponseStatus }, + RegisterAsset { status: ResponseStatus }, + Allocate { status: ResponseStatus }, + Adapter(adapter::HandleAnswer), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + Config {}, + Assets {}, + Allocations { asset: HumanAddr }, + PendingAllowance { asset: HumanAddr }, + Adapter(adapter::SubQueryMsg), +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = 256; +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryAnswer { + Config { config: Config }, + Assets { assets: Vec }, + Allocations { allocations: Vec }, + PendingAllowance { amount: Uint128 }, + Adapter(adapter::QueryAnswer), +} diff --git a/packages/shade_protocol/src/utils/asset.rs b/packages/shade_protocol/src/utils/asset.rs index 6b6cd8196..cdd34bdd3 100644 --- a/packages/shade_protocol/src/utils/asset.rs +++ b/packages/shade_protocol/src/utils/asset.rs @@ -1,4 +1,7 @@ -use cosmwasm_std::HumanAddr; +use cosmwasm_std::{ + HumanAddr, Uint128, BankQuery, BalanceResponse, + Extern, Storage, Api, Querier, StdResult, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,3 +11,20 @@ pub struct Contract { pub address: HumanAddr, pub code_hash: String, } + +pub fn scrt_balance( + deps: &Extern, + address: HumanAddr, +) -> StdResult { + + let resp: BalanceResponse = deps.querier.query( + &BankQuery::Balance { + address, + denom: "uscrt".to_string(), + } + .into(), + )?; + + Ok(resp.amount.amount) +} + diff --git a/packages/shade_protocol/src/utils/cycle.rs b/packages/shade_protocol/src/utils/cycle.rs new file mode 100644 index 000000000..1f475ef4b --- /dev/null +++ b/packages/shade_protocol/src/utils/cycle.rs @@ -0,0 +1,106 @@ +use cosmwasm_std::{ + Uint128, StdResult, StdError, Env, +}; +use crate::{ + utils::{ + asset::Contract, + generic_response::ResponseStatus + }, +}; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use schemars::JsonSchema; +use std::convert::TryInto; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Cycle { + Once, + Constant, + /* + Block { + blocks: Uint128, + }, + */ + Yearly { + years: Uint128, + }, + Monthly { + months: Uint128, + }, + Daily { + days: Uint128, + }, + Hourly { + hours: Uint128, + }, + Minutes { + minutes: Uint128, + }, + Seconds { + seconds: Uint128, + }, +} + +pub fn parse_utc_datetime( + rfc3339: &String, +) -> StdResult> { + + DateTime::parse_from_rfc3339(&rfc3339) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|_| + StdError::generic_err( + format!("Failed to parse rfc3339 datetime {}", rfc3339) + ) + ) +} + +pub fn utc_now( + env: &Env, +) -> DateTime { + DateTime::from_utc( + NaiveDateTime::from_timestamp(env.block.time as i64, 0), + Utc, + ) +} + +pub fn exceeds_cycle( + now: &DateTime, + last_refresh: &DateTime, + cycle: Cycle, +) -> bool { + + match cycle { + Cycle::Constant => true, + Cycle::Once => false, + //Cycle::Block { blocks } => {}, + Cycle::Seconds { seconds } => { + seconds >= Uint128((now.timestamp() - last_refresh.timestamp()) as u128) + }, + Cycle::Minutes { minutes } => { + minutes >= Uint128(((now.timestamp() - last_refresh.timestamp()) / 60).try_into().unwrap()) + }, + Cycle::Hourly { hours } => { + hours >= Uint128(((now.timestamp() - last_refresh.timestamp()) / 60 / 60).try_into().unwrap()) + }, + Cycle::Daily { days } => { + now.num_days_from_ce() - last_refresh.num_days_from_ce() >= days.u128() as i32 + }, + Cycle::Monthly { months } => { + let mut month_diff = 0u32; + + if now.year() > last_refresh.year() { + month_diff = (12u32 - last_refresh.month()) + now.month(); + } + else { + month_diff = now.month() - last_refresh.month(); + } + + month_diff >= months.u128() as u32 + }, + Cycle::Yearly { years } => { + now.year_ce() > last_refresh.year_ce() + }, + } + +} diff --git a/packages/shade_protocol/src/utils/mod.rs b/packages/shade_protocol/src/utils/mod.rs index 14d8ccb72..a9c40fafb 100644 --- a/packages/shade_protocol/src/utils/mod.rs +++ b/packages/shade_protocol/src/utils/mod.rs @@ -14,5 +14,10 @@ pub mod generic_response; pub mod storage; +#[cfg(feature = "utils")] +pub mod cycle; +#[cfg(feature = "utils")] +pub mod wrap; + #[cfg(feature = "math")] pub mod price; diff --git a/packages/shade_protocol/src/utils/price.rs b/packages/shade_protocol/src/utils/price.rs index 3fca68bc3..be440f32b 100644 --- a/packages/shade_protocol/src/utils/price.rs +++ b/packages/shade_protocol/src/utils/price.rs @@ -1,4 +1,4 @@ -use cosmwasm_math_compat::Uint128; +use cosmwasm_std::Uint128; use std::convert::TryFrom; /* Translate price from symbol/sSCRT -> symbol/USD @@ -22,7 +22,8 @@ pub fn normalize_price(amount: Uint128, decimals: u8) -> Uint128 { #[cfg(test)] mod tests { use super::*; - use cosmwasm_math_compat::Uint128; + use cosmwasm_std::Uint128; + macro_rules! normalize_price_tests { ($($name:ident: $value:expr,)*) => { $( diff --git a/packages/shade_protocol/src/utils/wrap.rs b/packages/shade_protocol/src/utils/wrap.rs new file mode 100644 index 000000000..6be1af719 --- /dev/null +++ b/packages/shade_protocol/src/utils/wrap.rs @@ -0,0 +1,71 @@ +use cosmwasm_std::{ + Uint128, StdResult, StdError, Binary, + CosmosMsg, Storage, Querier, HumanAddr, + Api, +}; +use crate::{ + utils::{ + asset::Contract, + generic_response::ResponseStatus + }, +}; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use schemars::JsonSchema; +use std::convert::TryInto; +use secret_toolkit::snip20::{deposit_msg, redeem_msg, send_msg}; + +pub fn wrap( + amount: Uint128, + token: Contract, + //denom: Option, +) -> StdResult { + + Ok(deposit_msg( + amount, + None, + 256, + token.code_hash, + token.address, + )?) +} + +pub fn wrap_and_send( + amount: Uint128, + recipient: HumanAddr, + token: Contract, + //denom: Option, + msg: Option, +) -> StdResult> { + + Ok(vec![ + wrap(amount, token.clone())?, + send_msg( + recipient, + amount, + msg, + None, + None, + 256, + token.code_hash.clone(), + token.address.clone(), + )? + ]) +} + +pub fn unwrap( + amount: Uint128, + token: Contract, + //denom: Option, +) -> StdResult { + + + Ok(redeem_msg( + amount, + None, + None, + 256, + token.code_hash.clone(), + token.address.clone(), + )?) +} diff --git a/test-scrt-staking.py b/test-scrt-staking.py index 94f95284e..536b3f897 100755 --- a/test-scrt-staking.py +++ b/test-scrt-staking.py @@ -6,202 +6,101 @@ from contractlib.secretlib.secretlib import run_command, execute_contract, query_contract from contractlib.snip20lib import SNIP20 -''' -chain_config = run_command(['secretd', 'config']) - -chain_config = { - key.strip('" '): val.strip('" ') - for key, val in - ( - line.split('=') - for line in chain_config.split('\n') - if line - ) -} -''' - viewing_key = 'password' - -account_key = 'a' #if chain_config['chain-id'] == 'holodeck-2' else 'a' +ACCOUNT_KEY = 'a' #if chain_config['chain-id'] == 'holodeck-2' else 'a' backend = 'test' #None if chain_config['chain-id'] == 'holodeck-2' else 'test' -account = run_command(['secretd', 'keys', 'show', '-a', account_key]).rstrip() +ACCOUNT = run_command(['secretd', 'keys', 'show', '-a', ACCOUNT_KEY]).rstrip() - -print('ACCOUNT', account) +print('ACCOUNT', ACCOUNT) print('Configuring sSCRT') sscrt = SNIP20(gen_label(8), name='secretSCRT', symbol='SSCRT', decimals=6, public_total_supply=True, enable_deposit=True, enable_burn=True, - enable_redeem=True, admin=account, - uploader=account, backend=backend) + enable_redeem=True, admin=ACCOUNT, + uploader=ACCOUNT, backend=backend) print(sscrt.address) sscrt.execute({'set_viewing_key': {'key': viewing_key}}) -deposit_amount = '200000000uscrt' -# lol -half_amount = '100000000uscrt' +# 200 +#deposit_amount = '200000000' +# 10 +deposit_amount = '10000000' print('Depositing', deposit_amount) -sscrt.execute({'deposit': {}}, account, deposit_amount) -print('SSCRT', sscrt.get_balance(account, viewing_key)) +sscrt.execute({'deposit': {}}, ACCOUNT, deposit_amount + 'uscrt') +print('SSCRT', sscrt.get_balance(ACCOUNT, viewing_key)) -''' -treasury = Contract( - '../compiled/treasury.wasm.gz', +scrt_staking = Contract( + '../compiled/scrt_staking.wasm.gz', json.dumps({ - 'admin': account, + 'admin': ACCOUNT, + 'treasury': ACCOUNT, + 'sscrt': { + 'address': sscrt.address, + 'code_hash': sscrt.code_hash, + }, 'viewing_key': viewing_key, }), gen_label(8), ) -print('TREASURY', treasury.address) -''' - -staking_init = { - 'admin': account, - 'treasury': account, - 'sscrt': { - 'address': sscrt.address, - 'code_hash': sscrt.code_hash, - }, - 'viewing_key': viewing_key, -} - -scrt_staking = Contract( - '../compiled/scrt_staking.wasm.gz', - json.dumps(staking_init), - gen_label(8), -) print('STAKING', scrt_staking.address) -''' -print('Configuring treasury') -print(treasury.execute({ - 'register_asset': { - 'contract': { - 'address': sscrt.address, - 'code_hash': sscrt.code_hash, - } - } -})) - -print(treasury.execute({ - 'register_allocation': { - 'asset': sscrt.address, - 'allocation': { - 'staking': { - 'contract': { - 'address': scrt_staking.address, - 'code_hash': scrt_staking.code_hash, - }, - 'allocation': '100000000000000000', # 0.1 - }, - } - } -})) - - -print('Treasury sSCRT Balance') -print(treasury.query({'balance': {'asset': sscrt.address}})) - -print('Treasury sSCRT Applications') -print(treasury.query({'allocations': {'asset': sscrt.address}})) - -#print('config') -#print(scrt_staking.query({'config': {}})) -''' -print('Sending 100000000 usscrt direct to staking') -sscrt.execute({ +print(f'Sending {deposit_amount} usscrt direct to staking') +print(sscrt.execute({ "send": { "recipient": scrt_staking.address, - "amount": str(100000000), + "amount": deposit_amount, }, }, - account, -) + ACCOUNT, +)) -''' -print('staking sscrt') -print(sscrt.get_balance(scrt_staking.address, viewing_key)) -''' - -print('DELEGATIONS') -delegations = scrt_staking.query({'delegations': {}}) -print(delegations) - - -sleep(3) -scrt_balance = json.loads(run_command(['secretd', 'q', 'bank', 'balances', account])) -print('SCRT', scrt_balance['balances'][0]['amount']) -print('SSCRT', sscrt.get_balance(account, viewing_key)) - -while scrt_staking.query({'rewards': {}}) == 0: - pass - -print('REWARDS', scrt_staking.query({'rewards': {}})) -''' -for delegation in delegations: - print(json.dumps({'delegation': {'validator': delegation['validator']}})) - print(scrt_staking.query({'delegation': {'validator': delegation['validator']}})) -''' - -''' -print('Treasury sSCRT Balance') -print(treasury.query({'balance': {'asset': sscrt.address}})) -''' - -#print('BALANCES') -#print(sscrt.query({'balance': {'address': scrt_staking.address, 'key': viewing_key}})) -#print(run_command(['secretd', 'q', 'account', scrt_staking.address])) - -print('CLAIMING') -for delegation in delegations: - print(scrt_staking.execute({'claim': {'validator': delegation['validator']}})) - -scrt_balance = json.loads(run_command(['secretd', 'q', 'bank', 'balances', account])) -print('SCRT', scrt_balance['balances'][0]['amount']) -print('SSCRT', sscrt.get_balance(account, viewing_key)) -print('REWARDS', scrt_staking.query({'rewards': {}})) - -print('UNBONDING') -for delegation in delegations: - print(scrt_staking.execute({'unbond': {'validator': delegation['validator']}})) - -print('CLAIMING') -for delegation in scrt_staking.query({'delegations': {}}): - print(scrt_staking.execute({'claim': {'validator': delegation['validator']}})) - -print('DELEGATIONS') -delegations = scrt_staking.query({'delegations': {}}) -print(delegations) - -print('SCRT', scrt_balance['balances'][0]['amount']) -print('SSCRT', sscrt.get_balance(account, viewing_key)) -print('REWARDS', scrt_staking.query({'rewards': {}})) - - -''' -for i in range(3): - print('Sending 100000000 usscrt to treasury') - print(sscrt.execute({ - "send": { - "recipient": treasury.address, - "amount": str(100000000), - }, - }, - account, - )) +while True: - print('Treasury sSCRT Balance') - print(treasury.query({'balance': {'asset': sscrt.address}})) + #print('user sSCRT', sscrt.get_balance(ACCOUNT, viewing_key)) + print('DELEGATIONS') delegations = scrt_staking.query({'delegations': {}}) print(delegations) - print('DELEGATIONS') - for delegation in delegations: - print(scrt_staking.query({'delegation': {'validator': delegation['validator']}})) -''' + print('L1 bal') + print(json.loads(run_command(['secretd', 'q', 'bank', 'balances', scrt_staking.address]))) + + print('Balance') + balance = scrt_staking.query({'adapter': {'balance': {'asset': sscrt.address}}})['balance']['amount'] + print(balance) + + #unbond_amount = str(int(10 * 10**6)) + unbond_amount = str(int(int(balance) * .8)) + + print('Unbond', unbond_amount) + print(scrt_staking.execute({'adapter': {'unbond': {'asset': sscrt.address, 'amount': unbond_amount}}})) + + print('Unbonding') + print(scrt_staking.query({'adapter': {'unbonding': {'asset': sscrt.address}}})) + + print('Balance') + balance = scrt_staking.query({'adapter': {'balance': {'asset': sscrt.address}}})['balance']['amount'] + print(balance) + + print('Updating') + print(scrt_staking.execute({'adapter': {'update': {}}})) + + print('Claimable') + print(scrt_staking.query({'adapter': {'claimable': {'asset': sscrt.address}}})) + + print('Claiming') + print(scrt_staking.execute({'adapter': {'claim': {'asset': sscrt.address}}})) + + ''' + print('Waiting on claimable', end='') + while scrt_staking.query({'adapter': {'claimable': {'asset': sscrt.address}}})['amount'] == '0': + print('.', end='') + pass + ''' + print() + print('=' * 15) + print() diff --git a/test-treasury-synthesis.py b/test-treasury-synthesis.py index 4272dacc2..4f611a8f0 100755 --- a/test-treasury-synthesis.py +++ b/test-treasury-synthesis.py @@ -23,121 +23,248 @@ viewing_key = 'password' -account_key = 'a' #if chain_config['chain-id'] == 'holodeck-2' else 'a' +ACCOUNT_KEY = 'a' #if chain_config['chain-id'] == 'holodeck-2' else 'a' backend = 'test' #None if chain_config['chain-id'] == 'holodeck-2' else 'test' -account = run_command(['secretd', 'keys', 'show', '-a', account_key]).rstrip() +ACCOUNT = run_command(['secretd', 'keys', 'show', '-a', ACCOUNT_KEY]).rstrip() -print('ACCOUNT', account) +print('ACCOUNT', ACCOUNT) print('Configuring sSCRT') sscrt = SNIP20(gen_label(8), name='secretSCRT', symbol='SSCRT', decimals=6, public_total_supply=True, enable_deposit=True, enable_burn=True, - enable_redeem=True, admin=account, - uploader=account, backend=backend) -print(sscrt.address) + enable_redeem=True, admin=ACCOUNT, + uploader=ACCOUNT, backend=backend) +print('sSCRT', sscrt.address, sscrt.code_hash) sscrt.execute({'set_viewing_key': {'key': viewing_key}}) -deposit_amount = '200000000uscrt' -# lol -half_amount = '100000000uscrt' +seed_amount = 100000000000 -print('Depositing', deposit_amount) -sscrt.execute({'deposit': {}}, account, deposit_amount) -print('SSCRT', sscrt.get_balance(account, viewing_key)) +print('Depositing', seed_amount) +sscrt.execute({'deposit': {}}, ACCOUNT, str(seed_amount) + 'uscrt') +print(f'Deploying Treasury') treasury = Contract( '../compiled/treasury.wasm.gz', json.dumps({ - 'admin': account, + 'admin': ACCOUNT, 'viewing_key': viewing_key, + 'sscrt': sscrt.as_dict(), }), gen_label(8), ) print('TREASURY', treasury.address) -staking_init = { - 'admin': account, - 'treasury': treasury.address, - 'sscrt': { - 'address': sscrt.address, - 'code_hash': sscrt.code_hash, - }, - 'viewing_key': viewing_key, -} +print('Registering Account w/ treasury') +print(treasury.execute({ + 'add_account': { + 'holder': ACCOUNT, + } +})) print('Registering sSCRT w/ treasury') print(treasury.execute({ 'register_asset': { - 'contract': { - 'address': sscrt.address, - 'code_hash': sscrt.code_hash, + 'contract': sscrt.as_dict(), + } +})) + +print('Deploying Manager') +treasury_manager = Contract( + '../compiled/treasury_manager.wasm.gz', + json.dumps({ + 'admin': ACCOUNT, + 'treasury': treasury.address, + 'viewing_key': viewing_key, + }), + gen_label(8), +) +print('Manager', treasury_manager.address) + +print('Registering sscrt w/ manager') +print(treasury_manager.execute({ + 'register_asset': { + 'contract': sscrt.as_dict(), + } + }, + ACCOUNT, +)) + +print(f'Registering Manager with Treasury') +print(treasury.execute({ + 'register_manager': { + 'contract': treasury_manager.as_dict(), + } +})) + +tolerance = .05 +allowance = .9 +print(f'Register Manager allowance {allowance * 100}% tolerance {tolerance * 100}%') +print(treasury.execute({ + 'allowance': { + 'asset': sscrt.address, + 'allowance': { + 'portion': { + 'spender': treasury_manager.address, + 'portion': str(int(allowance * 10**18)), + 'last_refresh': '', + 'tolerance': str(int(tolerance * 10**18)), + } } } })) +print('Deploying SCRT Staking') scrt_staking = Contract( '../compiled/scrt_staking.wasm.gz', - json.dumps(staking_init), + json.dumps({ + 'admin': ACCOUNT, + 'treasury': treasury.address, + 'sscrt': sscrt.as_dict(), + 'viewing_key': viewing_key, + }), gen_label(8), ) -print('STAKING', scrt_staking.address) +print(scrt_staking.address) -print('Allocating 90% sSCRT to staking') -allocation = .9 -print(treasury.execute({ - 'register_allocation': { +allocation = 1 + +print(f'Allocating {allocation * 100}% sSCRT to scrt-staking') +print(treasury_manager.execute({ + 'allocate': { 'asset': sscrt.address, 'allocation': { - 'staking': { - 'contract': { - 'address': scrt_staking.address, - 'code_hash': scrt_staking.code_hash, - }, - 'allocation': str(int(allocation * 10**18)), - }, + 'nick': 'SCRT Staking', + 'contract': scrt_staking.as_dict(), + 'alloc_type': 'portion', + 'amount': str(int(allocation * 10**18)), } } })) - print('Treasury Assets') print(treasury.query({'assets': {}})) print('Treasury sSCRT Balance') -print(treasury.query({'balance': {'asset': sscrt.address}})) - -print('Treasury sSCRT Applications') -print(treasury.query({'allocations': {'asset': sscrt.address}})) +print(treasury.query({'adapter': {'balance': {'asset': sscrt.address}}})) -print('Sending 100000000 usscrt to treasury') -sscrt.execute({ +print(f'Sending {seed_amount} usscrt to treasury') +print(sscrt.execute({ "send": { "recipient": treasury.address, - "amount": str(100000000), + "amount": str(seed_amount), }, }, - account, -) -print('Treasury sSCRT Balance') -print(treasury.query({'balance': {'asset': sscrt.address}})) + ACCOUNT, +)) -print('DELEGATIONS') -delegations = scrt_staking.query({'delegations': {}}) -print(delegations) -print('Waiting for rewards',) -while scrt_staking.query({'rewards': {}}) == '0': - print('.',) -print() +while True: + + print('\nTreasury') + print('Balance') + treasury_balance = treasury.query({ + 'adapter': { + 'balance': { + 'asset': sscrt.address + }, + } + })['balance']['amount'] + print(treasury_balance) + + print('\nManager') + + print('Balance') + manager_balance = treasury_manager.query({ + 'adapter': { + 'balance': { + 'asset': sscrt.address, + } + } + })['balance']['amount'] + print(manager_balance) + + outstanding = sum(map(int, [manager_balance])) + reserves = int(treasury_balance) - outstanding + + print('ALLOCS') + print('Manager', int(manager_balance) / int(treasury_balance)) + print('Reserves', int(reserves) / int(treasury_balance)) -print('REWARDS', scrt_staking.query({'rewards': {}})) + print('Rebalance...') + print(treasury.execute({ + 'adapter': { + 'update': { + 'asset': sscrt.address + }, + } + })) + print(treasury_manager.query({ + 'pending_allowance': { + 'asset': sscrt.address + } + })) -print('CLAIMING') -for delegation in delegations: - print(scrt_staking.execute({'claim': {'validator': delegation['validator']}})) + print('Unbonding') + unbonding = treasury_manager.query({ + 'adapter': { + 'unbonding': { + 'asset': sscrt.address, + } + } + })['unbonding']['amount'] + print(unbonding) + + print('Update Manager...') + treasury_manager.execute({ + 'adapter': { + 'update': { + 'asset': sscrt.address, + } + } + }, ACCOUNT) + + print('Update SCRT Staking...') + scrt_staking.execute({ + 'adapter': { + 'update': { + 'asset': sscrt.address, + } + } + }, ACCOUNT) + + print(treasury_manager.query({ + 'pending_allowance': { + 'asset': sscrt.address + } + })) + + print(treasury_manager.query({ + 'adapter': { + 'unbonding': { + 'asset': sscrt.address, + } + } + })) + + claimable = treasury_manager.query({ + 'adapter': { + 'claimable': { + 'asset': sscrt.address, + } + } + }) + print(claimable) + if claimable['claimable']['amount'] != '0': + print('Claiming...') + treasury_manager.execute({ + 'adapter': { + 'claim': {'asset': sscrt.address} + } + }) + + + print('=' * 20, end='\n') -print('Treasury sSCRT Balance') -print(treasury.query({'balance': {'asset': sscrt.address}}))