diff --git a/Cargo.lock b/Cargo.lock index 7967a2a3f..1e8d1961c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,7 +412,7 @@ dependencies = [ [[package]] name = "astroport-staking" -version = "1.0.2" +version = "1.1.0" dependencies = [ "astroport 2.7.1", "astroport-token", diff --git a/contracts/pair_astro_xastro/tests/integration.rs b/contracts/pair_astro_xastro/tests/integration.rs index 14a861d44..1f5979740 100644 --- a/contracts/pair_astro_xastro/tests/integration.rs +++ b/contracts/pair_astro_xastro/tests/integration.rs @@ -416,7 +416,7 @@ fn test_pair_swap() { &[], ) .unwrap(); - assert_user_balance(&mut router, &contracts.xastro_instance, &user1, 10_000u64); + assert_user_balance(&mut router, &contracts.xastro_instance, &user1, 9_000u64); router .execute_contract( @@ -537,7 +537,7 @@ fn test_pair_swap() { contracts.xastro_instance.clone(), &Cw20ExecuteMsg::Send { contract: contracts.pair_instance.clone().to_string(), - amount: Uint128::from(10_000u64), + amount: Uint128::from(9_000u64), msg: to_binary(&Cw20HookMsg::Swap { ask_asset_info: None, belief_price: None, @@ -549,7 +549,7 @@ fn test_pair_swap() { &[], ) .unwrap(); - assert_user_balance(&mut router, &contracts.astro_instance, &user1, 10_000u64); + assert_user_balance(&mut router, &contracts.astro_instance, &user1, 9_000u64); router .execute_contract( diff --git a/contracts/tokenomics/generator/tests/integration.rs b/contracts/tokenomics/generator/tests/integration.rs index b88199ca6..75bfd294e 100644 --- a/contracts/tokenomics/generator/tests/integration.rs +++ b/contracts/tokenomics/generator/tests/integration.rs @@ -93,7 +93,7 @@ fn test_boost_checkpoints_with_delegation() { // Create short lock user1 helper_controller .escrow_helper - .mint_xastro(&mut app, USER1, 100); + .mint_xastro(&mut app, USER1, 200); helper_controller .escrow_helper @@ -491,7 +491,7 @@ fn test_boost_checkpoints() { // Create short lock user1 helper_controller .escrow_helper - .mint_xastro(&mut app, USER1, 100); + .mint_xastro(&mut app, USER1, 200); helper_controller .escrow_helper @@ -3656,7 +3656,7 @@ fn test_proxy_generator_incorrect_virtual_amount() { ); helper_controller .escrow_helper - .mint_xastro(&mut app, USER1, 100); + .mint_xastro(&mut app, USER1, 200); helper_controller .escrow_helper .create_lock(&mut app, USER1, WEEK * 3, 100f32) diff --git a/contracts/tokenomics/staking/Cargo.toml b/contracts/tokenomics/staking/Cargo.toml index 8dde4032f..17d72fe8a 100644 --- a/contracts/tokenomics/staking/Cargo.toml +++ b/contracts/tokenomics/staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-staking" -version = "1.0.2" +version = "1.1.0" authors = ["Astroport"] edition = "2021" diff --git a/contracts/tokenomics/staking/src/contract.rs b/contracts/tokenomics/staking/src/contract.rs index 9c27faa67..c6d52334d 100644 --- a/contracts/tokenomics/staking/src/contract.rs +++ b/contracts/tokenomics/staking/src/contract.rs @@ -1,6 +1,7 @@ use cosmwasm_std::{ - entry_point, from_binary, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Reply, ReplyOn, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, + attr, entry_point, from_binary, to_binary, wasm_execute, Addr, Binary, CosmosMsg, Deps, + DepsMut, Env, MessageInfo, Reply, ReplyOn, Response, StdError, StdResult, SubMsg, Uint128, + WasmMsg, }; use crate::error::ContractError; @@ -28,6 +29,9 @@ const TOKEN_SYMBOL: &str = "xASTRO"; /// A `reply` call code ID used for sub-messages. const INSTANTIATE_TOKEN_REPLY_ID: u64 = 1; +/// Minimum initial xastro share +pub(crate) const MINIMUM_STAKE_AMOUNT: Uint128 = Uint128::new(1_000); + /// Creates a new contract with the specified parameters in the [`InstantiateMsg`]. #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -131,41 +135,72 @@ fn receive_cw20( let config: Config = CONFIG.load(deps.storage)?; let recipient = cw20_msg.sender; - let amount = cw20_msg.amount; + let mut amount = cw20_msg.amount; let mut total_deposit = query_token_balance( &deps.querier, &config.astro_token_addr, - env.contract.address, + env.contract.address.clone(), )?; let total_shares = query_supply(&deps.querier, &config.xastro_token_addr)?; match from_binary(&cw20_msg.msg)? { Cw20HookMsg::Enter {} => { + let mut messages = vec![]; if info.sender != config.astro_token_addr { return Err(ContractError::Unauthorized {}); } + // In a CW20 `send`, the total balance of the recipient is already increased. // To properly calculate the total amount of ASTRO deposited in staking, we should subtract the user deposit from the pool total_deposit -= amount; let mint_amount: Uint128 = if total_shares.is_zero() || total_deposit.is_zero() { + amount = amount + .checked_sub(MINIMUM_STAKE_AMOUNT) + .map_err(|_| ContractError::MinimumStakeAmountError {})?; + + // amount cannot become zero after minimum stake subtraction + if amount.is_zero() { + return Err(ContractError::MinimumStakeAmountError {}); + } + + messages.push(wasm_execute( + config.xastro_token_addr.clone(), + &Cw20ExecuteMsg::Mint { + recipient: env.contract.address.to_string(), + amount: MINIMUM_STAKE_AMOUNT, + }, + vec![], + )?); + amount } else { - amount + amount = amount .checked_mul(total_shares)? - .checked_div(total_deposit)? + .checked_div(total_deposit)?; + + if amount.is_zero() { + return Err(ContractError::StakeAmountTooSmall {}); + } + + amount }; - let res = Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: config.xastro_token_addr.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Mint { - recipient, + messages.push(wasm_execute( + config.xastro_token_addr, + &Cw20ExecuteMsg::Mint { + recipient: recipient.clone(), amount: mint_amount, - })?, - funds: vec![], - })); + }, + vec![], + )?); - Ok(res) + Ok(Response::new().add_messages(messages).add_attributes(vec![ + attr("action", "enter"), + attr("recipient", recipient), + attr("astro_amount", cw20_msg.amount), + attr("xastro_amount", mint_amount), + ])) } Cw20HookMsg::Leave {} => { if info.sender != config.xastro_token_addr { @@ -186,13 +221,18 @@ fn receive_cw20( .add_message(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.astro_token_addr.to_string(), msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient, + recipient: recipient.clone(), amount: what, })?, funds: vec![], })); - Ok(res) + Ok(res.add_attributes(vec![ + attr("action", "leave"), + attr("recipient", recipient), + attr("xastro_amount", cw20_msg.amount), + attr("astro_amount", what), + ])) } } } @@ -238,7 +278,7 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result match contract_version.version.as_ref() { - "1.0.0" | "1.0.1" => {} + "1.0.0" | "1.0.1" | "1.0.2" => {} _ => return Err(ContractError::MigrationError {}), }, _ => return Err(ContractError::MigrationError {}), diff --git a/contracts/tokenomics/staking/src/error.rs b/contracts/tokenomics/staking/src/error.rs index 4ebe58ef4..8a869d6f6 100644 --- a/contracts/tokenomics/staking/src/error.rs +++ b/contracts/tokenomics/staking/src/error.rs @@ -1,3 +1,4 @@ +use crate::contract::MINIMUM_STAKE_AMOUNT; use cosmwasm_std::{DivideByZeroError, OverflowError, StdError}; use thiserror::Error; @@ -12,6 +13,12 @@ pub enum ContractError { #[error("An error occurred during migration")] MigrationError {}, + + #[error("Initial stake amount must be more than {}", MINIMUM_STAKE_AMOUNT)] + MinimumStakeAmountError {}, + + #[error("Insufficient amount of Stake")] + StakeAmountTooSmall {}, } impl From for ContractError { diff --git a/contracts/tokenomics/staking/tests/integration.rs b/contracts/tokenomics/staking/tests/integration.rs index 62b10166e..5c9fd8be8 100644 --- a/contracts/tokenomics/staking/tests/integration.rs +++ b/contracts/tokenomics/staking/tests/integration.rs @@ -7,6 +7,115 @@ use cw_multi_test::{App, ContractWrapper, Executor}; const ALICE: &str = "alice"; const BOB: &str = "bob"; const CAROL: &str = "carol"; +const ATTACKER: &str = "attacker"; +const VICTIM: &str = "victim"; + +#[test] +fn check_deflate_liquidity() { + let mut router = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (astro_token_instance, staking_instance, _) = + instantiate_contracts(&mut router, owner.clone()); + + mint_some_astro( + &mut router, + owner.clone(), + astro_token_instance.clone(), + ATTACKER, + ); + + mint_some_astro( + &mut router, + owner.clone(), + astro_token_instance.clone(), + VICTIM, + ); + + let attacker_address = Addr::unchecked(ATTACKER); + let victim_address = Addr::unchecked(VICTIM); + + let msg = Cw20ExecuteMsg::Send { + contract: staking_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), + amount: Uint128::from(1000u128), + }; + + let err = router + .execute_contract( + attacker_address.clone(), + astro_token_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Initial stake amount must be more than 1000" + ); + + let msg = Cw20ExecuteMsg::Send { + contract: staking_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), + amount: Uint128::from(1001u128), + }; + + router + .execute_contract( + attacker_address.clone(), + astro_token_instance.clone(), + &msg, + &[], + ) + .unwrap(); + + let msg = Cw20ExecuteMsg::Transfer { + recipient: staking_instance.to_string(), + amount: Uint128::from(5000u128), + }; + + router + .execute_contract( + attacker_address.clone(), + astro_token_instance.clone(), + &msg, + &[], + ) + .unwrap(); + + let msg = Cw20ExecuteMsg::Send { + contract: staking_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), + amount: Uint128::from(2u128), + }; + + let err = router + .execute_contract( + victim_address.clone(), + astro_token_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Insufficient amount of Stake"); + + let msg = Cw20ExecuteMsg::Send { + contract: staking_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), + amount: Uint128::from(10u128), + }; + + router + .execute_contract( + victim_address.clone(), + astro_token_instance.clone(), + &msg, + &[], + ) + .unwrap(); +} fn mock_app() -> App { App::default() @@ -98,7 +207,7 @@ fn instantiate_contracts(router: &mut App, owner: Addr) -> (Addr, Addr, Addr) { fn mint_some_astro(router: &mut App, owner: Addr, astro_token_instance: Addr, to: &str) { let msg = cw20::Cw20ExecuteMsg::Mint { recipient: String::from(to), - amount: Uint128::from(100u128), + amount: Uint128::from(10000u128), }; let res = router .execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) @@ -107,7 +216,7 @@ fn mint_some_astro(router: &mut App, owner: Addr, astro_token_instance: Addr, to assert_eq!(res.events[1].attributes[2], attr("to", String::from(to))); assert_eq!( res.events[1].attributes[3], - attr("amount", Uint128::from(100u128)) + attr("amount", Uint128::from(10000u128)) ); } @@ -120,7 +229,7 @@ fn cw20receive_enter_and_leave() { let (astro_token_instance, staking_instance, x_astro_token_instance) = instantiate_contracts(&mut router, owner.clone()); - // Mint 100 ASTRO for Alice + // Mint 10000 ASTRO for Alice mint_some_astro( &mut router, owner.clone(), @@ -142,7 +251,7 @@ fn cw20receive_enter_and_leave() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(100u128) + balance: Uint128::from(10000u128) } ); @@ -163,11 +272,11 @@ fn cw20receive_enter_and_leave() { .unwrap_err(); assert_eq!(resp.root_cause().to_string(), "Unauthorized"); - // Tru to stake Alice's 100 ASTRO for 100 xASTRO + // Tru to stake Alice's 1100 ASTRO for 1100 xASTRO let msg = Cw20ExecuteMsg::Send { contract: staking_instance.to_string(), msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), - amount: Uint128::from(100u128), + amount: Uint128::from(1100u128), }; router @@ -179,7 +288,7 @@ fn cw20receive_enter_and_leave() { ) .unwrap(); - // Check if Alice's xASTRO balance is 100 + // Check if Alice's xASTRO balance is 1100 let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -195,7 +304,7 @@ fn cw20receive_enter_and_leave() { } ); - // Check if Alice's ASTRO balance is 0 + // Check if Alice's ASTRO balance is 8900 let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -207,11 +316,11 @@ fn cw20receive_enter_and_leave() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(0u128) + balance: Uint128::from(8900u128) } ); - // Check if the staking contract's ASTRO balance is 100 + // Check if the staking contract's ASTRO balance is 1100 let msg = Cw20QueryMsg::Balance { address: staking_instance.to_string(), }; @@ -223,7 +332,7 @@ fn cw20receive_enter_and_leave() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(100u128) + balance: Uint128::from(1100u128) } ); @@ -276,7 +385,7 @@ fn cw20receive_enter_and_leave() { } ); - // Check if Alice's ASTRO balance is 10 + // Check if Alice's ASTRO balance is 8910 let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -288,11 +397,11 @@ fn cw20receive_enter_and_leave() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(10u128) + balance: Uint128::from(8910u128) } ); - // Check if the staking contract's ASTRO balance is 90 + // Check if the staking contract's ASTRO balance is 1090 let msg = Cw20QueryMsg::Balance { address: staking_instance.to_string(), }; @@ -304,11 +413,11 @@ fn cw20receive_enter_and_leave() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(90u128) + balance: Uint128::from(1090u128) } ); - // Check if the staking contract's xASTRO balance is 0 + // Check if the staking contract's xASTRO balance is 1000 let msg = Cw20QueryMsg::Balance { address: staking_instance.to_string(), }; @@ -320,7 +429,7 @@ fn cw20receive_enter_and_leave() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(0u128) + balance: Uint128::from(1000u128) } ); @@ -328,12 +437,12 @@ fn cw20receive_enter_and_leave() { .wrap() .query_wasm_smart(staking_instance.clone(), &QueryMsg::TotalDeposit {}) .unwrap(); - assert_eq!(res.u128(), 90); + assert_eq!(res.u128(), 1090); let res: Uint128 = router .wrap() .query_wasm_smart(staking_instance, &QueryMsg::TotalShares {}) .unwrap(); - assert_eq!(res.u128(), 90); + assert_eq!(res.u128(), 1090); } #[test] @@ -345,7 +454,7 @@ fn should_not_allow_withdraw_more_than_what_you_have() { let (astro_token_instance, staking_instance, x_astro_token_instance) = instantiate_contracts(&mut router, owner.clone()); - // Mint 100 ASTRO for Alice + // Mint 10000 ASTRO for Alice mint_some_astro( &mut router, owner.clone(), @@ -354,11 +463,11 @@ fn should_not_allow_withdraw_more_than_what_you_have() { ); let alice_address = Addr::unchecked(ALICE); - // enter Alice's 100 ASTRO for 100 xASTRO + // enter Alice's 2000 ASTRO for 1000 xASTRO let msg = Cw20ExecuteMsg::Send { contract: staking_instance.to_string(), msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), - amount: Uint128::from(100u128), + amount: Uint128::from(2000u128), }; router @@ -370,7 +479,7 @@ fn should_not_allow_withdraw_more_than_what_you_have() { ) .unwrap(); - // Check if Alice's xASTRO balance is 100 + // Check if Alice's xASTRO balance is 1000 let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -382,15 +491,15 @@ fn should_not_allow_withdraw_more_than_what_you_have() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(100u128) + balance: Uint128::from(1000u128) } ); - // Try to burn Alice's 200 xASTRO and unstake + // Try to burn Alice's 2000 xASTRO and unstake let msg = Cw20ExecuteMsg::Send { contract: staking_instance.to_string(), msg: to_binary(&Cw20HookMsg::Leave {}).unwrap(), - amount: Uint128::from(200u128), + amount: Uint128::from(2000u128), }; let res = router @@ -402,7 +511,10 @@ fn should_not_allow_withdraw_more_than_what_you_have() { ) .unwrap_err(); - assert_eq!(res.root_cause().to_string(), "Cannot Sub with 100 and 200"); + assert_eq!( + res.root_cause().to_string(), + "Cannot Sub with 1000 and 2000" + ); } #[test] @@ -414,7 +526,7 @@ fn should_work_with_more_than_one_participant() { let (astro_token_instance, staking_instance, x_astro_token_instance) = instantiate_contracts(&mut router, owner.clone()); - // Mint 100 ASTRO for Alice + // Mint 10000 ASTRO for Alice mint_some_astro( &mut router, owner.clone(), @@ -423,7 +535,7 @@ fn should_work_with_more_than_one_participant() { ); let alice_address = Addr::unchecked(ALICE); - // Mint 100 ASTRO for Bob + // Mint 10000 ASTRO for Bob mint_some_astro( &mut router, owner.clone(), @@ -432,7 +544,7 @@ fn should_work_with_more_than_one_participant() { ); let bob_address = Addr::unchecked(BOB); - // Mint 100 ASTRO for Carol + // Mint 10000 ASTRO for Carol mint_some_astro( &mut router, owner.clone(), @@ -441,11 +553,11 @@ fn should_work_with_more_than_one_participant() { ); let carol_address = Addr::unchecked(CAROL); - // Stake Alice's 20 ASTRO for 20 xASTRO + // Stake Alice's 2000 ASTRO for 1000 xASTRO (subtract min liquid amount) let msg = Cw20ExecuteMsg::Send { contract: staking_instance.to_string(), msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), - amount: Uint128::from(20u128), + amount: Uint128::from(2000u128), }; router @@ -468,7 +580,7 @@ fn should_work_with_more_than_one_participant() { .execute_contract(bob_address.clone(), astro_token_instance.clone(), &msg, &[]) .unwrap(); - // Check if Alice's xASTRO balance is 20 + // Check if Alice's xASTRO balance is 1000 let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -480,7 +592,7 @@ fn should_work_with_more_than_one_participant() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(20u128) + balance: Uint128::from(1000u128) } ); @@ -500,7 +612,7 @@ fn should_work_with_more_than_one_participant() { } ); - // Check if staking contract's ASTRO balance is 30 + // Check if staking contract's ASTRO balance is 2010 let msg = Cw20QueryMsg::Balance { address: staking_instance.to_string(), }; @@ -512,7 +624,7 @@ fn should_work_with_more_than_one_participant() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(30u128) + balance: Uint128::from(2010u128) } ); @@ -540,7 +652,7 @@ fn should_work_with_more_than_one_participant() { attr("amount", Uint128::from(20u128)) ); - // Stake Alice's 10 ASTRO for 6 xASTRO: 10*30/50 = 6 + // Stake Alice's 10 ASTRO for 9 xASTRO: 10*2010/2030 = 9 let msg = Cw20ExecuteMsg::Send { contract: staking_instance.to_string(), msg: to_binary(&Cw20HookMsg::Enter {}).unwrap(), @@ -556,7 +668,7 @@ fn should_work_with_more_than_one_participant() { ) .unwrap(); - // Check if Alice's xASTRO balance is 26 + // Check if Alice's xASTRO balance is 1009 let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -568,7 +680,7 @@ fn should_work_with_more_than_one_participant() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(26u128) + balance: Uint128::from(1009u128) } ); @@ -588,7 +700,7 @@ fn should_work_with_more_than_one_participant() { } ); - // Burn Bob's 5 xASTRO and unstake: gets 5*60/36 = 8 ASTRO + // Burn Bob's 5 xASTRO and unstake: gets 5*2040/2019 = 5 ASTRO let msg = Cw20ExecuteMsg::Send { contract: staking_instance.to_string(), msg: to_binary(&Cw20HookMsg::Leave {}).unwrap(), @@ -604,7 +716,7 @@ fn should_work_with_more_than_one_participant() { ) .unwrap(); - // Check if Alice's xASTRO balance is 26 + // Check if Alice's xASTRO balance is 1009 let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -616,7 +728,7 @@ fn should_work_with_more_than_one_participant() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(26u128) + balance: Uint128::from(1009u128) } ); @@ -648,11 +760,11 @@ fn should_work_with_more_than_one_participant() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(52u128) + balance: Uint128::from(2035u128) } ); - // Check if Alice's ASTRO balance is 70 (100 minted - 20 entered - 10 entered) + // Check if Alice's ASTRO balance is 7990 (10000 minted - 2000 entered - 10 entered) let msg = Cw20QueryMsg::Balance { address: alice_address.to_string(), }; @@ -664,11 +776,11 @@ fn should_work_with_more_than_one_participant() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(70u128) + balance: Uint128::from(7990u128) } ); - // Check if Bob's ASTRO balance is 98 (100 minted - 10 entered + 8 by leaving) + // Check if Bob's ASTRO balance is 9995 (10000 minted - 10 entered + 5 by leaving) let msg = Cw20QueryMsg::Balance { address: bob_address.to_string(), }; @@ -680,7 +792,7 @@ fn should_work_with_more_than_one_participant() { assert_eq!( res.unwrap(), BalanceResponse { - balance: Uint128::from(98u128) + balance: Uint128::from(9995u128) } ); }