From 0a0999b43b22676c10d726717dbce2299f1139da Mon Sep 17 00:00:00 2001 From: nahem Date: Thu, 14 Mar 2024 16:28:33 +0100 Subject: [PATCH] fix(smart-contracts): move start bid to a different function, rename subdenom, update ownership using cw_ownable --- Cargo.lock | 2 + contracts/injective-auction-pool/Cargo.toml | 1 + .../injective-auction-pool/src/contract.rs | 66 ++--- contracts/injective-auction-pool/src/error.rs | 18 +- .../injective-auction-pool/src/executions.rs | 198 ++------------ .../injective-auction-pool/src/helpers.rs | 250 +++++++++++++++++- .../src/tests/unit_tests.rs | 87 ++++-- packages/injective_auction/Cargo.toml | 1 + .../injective_auction/src/auction_pool.rs | 10 +- 9 files changed, 381 insertions(+), 252 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2147abf..2791f93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,6 +504,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-ownable", "cw-paginate-storage", "cw-storage-plus", "cw-utils", @@ -522,6 +523,7 @@ version = "0.0.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-ownable", "osmosis-std-derive", "prost", "prost-types", diff --git a/contracts/injective-auction-pool/Cargo.toml b/contracts/injective-auction-pool/Cargo.toml index 03e01f9..05a3a13 100644 --- a/contracts/injective-auction-pool/Cargo.toml +++ b/contracts/injective-auction-pool/Cargo.toml @@ -37,6 +37,7 @@ prost = { workspace = true } cw-utils = { workspace = true } treasurechest = { workspace = true } cw-paginate-storage = { workspace = true } +cw-ownable = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/injective-auction-pool/src/contract.rs b/contracts/injective-auction-pool/src/contract.rs index 8ecf7f3..800a7a6 100644 --- a/contracts/injective-auction-pool/src/contract.rs +++ b/contracts/injective-auction-pool/src/contract.rs @@ -1,6 +1,4 @@ -use std::str::FromStr; - -use cosmwasm_std::{entry_point, Addr, Coin, Uint128}; +use cosmwasm_std::{entry_point, to_json_binary, Addr}; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; @@ -8,9 +6,9 @@ use injective_auction::auction_pool::{Config, ExecuteMsg, InstantiateMsg, QueryM use crate::error::ContractError; use crate::executions::{self, settle_auction}; -use crate::helpers::{query_current_auction, validate_percentage}; +use crate::helpers::{new_auction_round, validate_percentage}; use crate::queries; -use crate::state::{Auction, BIDDING_BALANCE, CONFIG, UNSETTLED_AUCTION}; +use crate::state::CONFIG; const CONTRACT_NAME: &str = "crates.io:injective-auction-pool"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -25,6 +23,16 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let owner = deps.api.addr_validate(&msg.owner.unwrap_or(info.sender.to_string()))?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(owner.to_string().as_str()))?; + + // Ensure that the contract is funded with at least the minimum balance + let amount = cw_utils::must_pay(&info, &msg.native_denom)?; + if amount < msg.min_balance { + return Err(ContractError::InsufficientFunds { + native_denom: msg.native_denom, + min_balance: msg.min_balance, + }); + } let whitelisted_addresses = msg .whitelisted_addresses @@ -35,8 +43,8 @@ pub fn instantiate( CONFIG.save( deps.storage, &Config { - owner, native_denom: msg.native_denom, + min_balance: msg.min_balance, token_factory_type: msg.token_factory_type.clone(), rewards_fee: validate_percentage(msg.rewards_fee)?, rewards_fee_addr: deps.api.addr_validate(&msg.rewards_fee_addr)?, @@ -47,42 +55,12 @@ pub fn instantiate( }, )?; - // fetch current auction details and save them in the contract state - let current_auction_round_response = query_current_auction(deps.as_ref())?; - - let auction_round = current_auction_round_response - .auction_round - .ok_or(ContractError::CurrentAuctionQueryError)?; - - let basket = current_auction_round_response - .amount - .iter() - .map(|coin| Coin { - amount: Uint128::from_str(&coin.amount).expect("Failed to parse coin amount"), - denom: coin.denom.clone(), - }) - .collect(); - - UNSETTLED_AUCTION.save( - deps.storage, - &Auction { - basket, - auction_round, - lp_subdenom: 1, - closing_time: current_auction_round_response.auction_closing_time(), - }, - )?; - - BIDDING_BALANCE.save(deps.storage, &Uint128::zero())?; - - // create a new denom for the current auction round - let msg = msg.token_factory_type.create_denom(env.contract.address.clone(), "1"); + let (messages, attributes) = new_auction_round(deps, &env, info, None, None)?; Ok(Response::default() - .add_message(msg) + .add_messages(messages) .add_attribute("action", "instantiate") - .add_attribute("auction_round", auction_round.to_string()) - .add_attribute("lp_subdenom", "1")) + .add_attributes(attributes)) } #[entry_point] @@ -94,7 +72,6 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::UpdateConfig { - owner, rewards_fee, rewards_fee_addr, whitelist_addresses, @@ -104,13 +81,16 @@ pub fn execute( deps, env, info, - owner, rewards_fee, rewards_fee_addr, whitelist_addresses, min_next_bid_increment_rate, min_return, ), + ExecuteMsg::UpdateOwnership(action) => { + cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; + Ok(Response::default()) + }, ExecuteMsg::TryBid { auction_round, basket_value, @@ -132,6 +112,10 @@ pub fn execute( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::Config {} => queries::query_config(deps), + QueryMsg::Ownership {} => { + let ownership = cw_ownable::get_ownership(deps.storage)?; + to_json_binary(&ownership) + }, QueryMsg::TreasureChestContracts { start_after, limit, diff --git a/contracts/injective-auction-pool/src/error.rs b/contracts/injective-auction-pool/src/error.rs index 4513d15..7b56400 100644 --- a/contracts/injective-auction-pool/src/error.rs +++ b/contracts/injective-auction-pool/src/error.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{Decimal, Instantiate2AddressError, OverflowError, StdError}; +use cosmwasm_std::{Decimal, Instantiate2AddressError, OverflowError, StdError, Uint128}; +use cw_ownable::OwnershipError; use cw_utils::PaymentError; use thiserror::Error; @@ -49,4 +50,19 @@ pub enum ContractError { #[error("Instantiate address error: {0}")] Instantiate2AddressError(#[from] Instantiate2AddressError), + + #[error("Missing auction winner")] + MissingAuctionWinner, + + #[error("Missing auction winning bid")] + MissingAuctionWinningBid, + + #[error("Insufficient funds. Must deposit at least {min_balance} {native_denom} to instantiate the contract")] + InsufficientFunds { + native_denom: String, + min_balance: Uint128, + }, + + #[error(transparent)] + Ownership(#[from] OwnershipError), } diff --git a/contracts/injective-auction-pool/src/executions.rs b/contracts/injective-auction-pool/src/executions.rs index 8d520b0..4a3a04e 100644 --- a/contracts/injective-auction-pool/src/executions.rs +++ b/contracts/injective-auction-pool/src/executions.rs @@ -1,11 +1,9 @@ -use std::str::FromStr; - -use crate::helpers::{query_current_auction, validate_percentage}; -use crate::state::{Auction, BIDDING_BALANCE, CONFIG, TREASURE_CHEST_CONTRACTS, UNSETTLED_AUCTION}; +use crate::helpers::{new_auction_round, query_current_auction, validate_percentage}; +use crate::state::{BIDDING_BALANCE, CONFIG, UNSETTLED_AUCTION}; use crate::ContractError; use cosmwasm_std::{ - coins, instantiate2_address, to_json_binary, Addr, BankMsg, Binary, CodeInfoResponse, Coin, - CosmosMsg, Decimal, DepsMut, Env, MessageInfo, OverflowError, Response, Uint128, WasmMsg, + coins, to_json_binary, Addr, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, + Uint128, WasmMsg, }; use injective_auction::auction::MsgBid; use injective_auction::auction_pool::ExecuteMsg::TryBid; @@ -16,22 +14,15 @@ pub fn update_config( deps: DepsMut, _env: Env, info: MessageInfo, - owner: Option, rewards_fee: Option, rewards_fee_addr: Option, whitelist_addresses: Option>, min_next_bid_increment_rate: Option, min_return: Option, ) -> Result { - let mut config = CONFIG.load(deps.storage)?; - - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } + cw_ownable::assert_owner(deps.storage, &info.sender)?; - if let Some(owner) = owner { - config.owner = deps.api.addr_validate(&owner)?; - } + let mut config = CONFIG.load(deps.storage)?; if let Some(rewards_fee) = rewards_fee { config.rewards_fee = validate_percentage(rewards_fee)?; @@ -60,7 +51,6 @@ pub fn update_config( Ok(Response::default() .add_attribute("action", "update_config") - .add_attribute("owner", config.owner.to_string()) .add_attribute("native_denom", config.native_denom) .add_attribute("token_factory_type", config.token_factory_type.to_string()) .add_attribute("rewards_fee", config.rewards_fee.to_string()) @@ -111,12 +101,12 @@ pub(crate) fn join_pool( let lp_subdenom = UNSETTLED_AUCTION.load(deps.storage)?.lp_subdenom; messages.push(config.token_factory_type.mint( env.contract.address.clone(), - lp_subdenom.to_string().as_str(), + format!("auction.{}", lp_subdenom).as_str(), amount, )); // send the minted lp token to the user - let lp_denom = format!("factory/{}/{}", env.contract.address, lp_subdenom); + let lp_denom = format!("factory/{}/auction.{}", env.contract.address, lp_subdenom); messages.push( BankMsg::Send { to_address: info.sender.to_string(), @@ -157,7 +147,7 @@ pub(crate) fn exit_pool( //make sure the user sends a correct amount and denom to exit the pool let lp_denom = format!( - "factory/{}/{}", + "factory/{}/auction.{}", env.contract.address, UNSETTLED_AUCTION.load(deps.storage)?.lp_subdenom ); @@ -312,169 +302,11 @@ pub fn settle_auction( return Err(ContractError::AuctionRoundHasNotFinished); } - // the contract won the auction - // NOTE: this is assuming the bot is sending the correct data about the winner of the previous auction - // currently there's no way to query the auction module directly to get this information - if deps.api.addr_validate(&auction_winner)? == env.contract.address { - // update LP subdenom for the next auction round (increment by 1) - let new_subdenom = unsettled_auction.lp_subdenom.checked_add(1).ok_or( - ContractError::OverflowError(OverflowError { - operation: cosmwasm_std::OverflowOperation::Add, - operand1: unsettled_auction.lp_subdenom.to_string(), - operand2: 1.to_string(), - }), - )?; - - let basket = unsettled_auction.basket; - let mut basket_fees = vec![]; - let mut basket_to_treasure_chest = vec![]; - - // add the unused bidding balance to the basket to be redeemed later - // TODO: should this be taxed though? if not, move after the for loop - let remaining_bidding_balance = - BIDDING_BALANCE.load(deps.storage)?.checked_sub(auction_winning_bid)?; - - if remaining_bidding_balance > Uint128::zero() { - basket_to_treasure_chest.push(Coin { - denom: config.native_denom.clone(), - amount: remaining_bidding_balance, - }); - } + let (messages, attributes) = + new_auction_round(deps, &env, info, Some(auction_winner), Some(auction_winning_bid))?; - // split the basket, taking the rewards fees into account - for coin in basket.iter() { - let fee = coin.amount * config.rewards_fee; - basket_fees.push(Coin { - denom: coin.denom.clone(), - amount: fee, - }); - basket_to_treasure_chest.push(Coin { - denom: coin.denom.clone(), - amount: coin.amount.checked_sub(fee)?, - }); - } - - // reset the bidding balance to 0 if we won, otherwise keep the balance for the next round - BIDDING_BALANCE.save(deps.storage, &Uint128::zero())?; - - let mut messages: Vec = vec![]; - - // transfer corresponding tokens to the rewards fee address - messages.push(CosmosMsg::Bank(BankMsg::Send { - to_address: config.rewards_fee_addr.to_string(), - amount: basket_fees, - })); - - // instantiate a treasury chest contract and get the future contract address - let creator = deps.api.addr_canonicalize(env.contract.address.as_str())?; - let code_id = config.treasury_chest_code_id; - - let CodeInfoResponse { - code_id: _, - creator: _, - checksum, - .. - } = deps.querier.query_wasm_code_info(code_id)?; - - let seed = format!( - "{}{}{}", - unsettled_auction.auction_round, - info.sender.into_string(), - env.block.height - ); - let salt = Binary::from(seed.as_bytes()); - - let treasure_chest_address = - Addr::unchecked(instantiate2_address(&checksum, &creator, &salt)?.to_string()); - - let denom = format!("factory/{}/{}", env.contract.address, unsettled_auction.lp_subdenom); - - messages.push(CosmosMsg::Wasm(WasmMsg::Instantiate2 { - admin: Some(env.contract.address.to_string()), - code_id, - label: format!("Treasure chest for auction round {}", unsettled_auction.auction_round), - msg: to_json_binary(&treasurechest::chest::InstantiateMsg { - denom: config.native_denom.clone(), - owner: env.contract.address.to_string(), - notes: denom.clone(), - token_factory: config.token_factory_type.to_string(), - burn_it: Some(false), - })?, - funds: basket_to_treasure_chest, - salt, - })); - - TREASURE_CHEST_CONTRACTS.save( - deps.storage, - unsettled_auction.auction_round, - &treasure_chest_address, - )?; - - // transfer previous token factory's admin rights to the treasury chest contract - messages.push(config.token_factory_type.change_admin( - env.contract.address.clone(), - &denom, - treasure_chest_address.clone(), - )); - - // create a new denom for the current auction round - messages.push( - config - .token_factory_type - .create_denom(env.contract.address, new_subdenom.to_string().as_str()), - ); - - let basket = current_auction_round_response - .amount - .iter() - .map(|coin| Coin { - amount: Uint128::from_str(&coin.amount).expect("Failed to parse coin amount"), - denom: coin.denom.clone(), - }) - .collect(); - - UNSETTLED_AUCTION.save( - deps.storage, - &Auction { - basket, - auction_round, - lp_subdenom: new_subdenom, - closing_time: current_auction_round_response.auction_closing_time(), - }, - )?; - - Ok(Response::default() - .add_messages(messages) - .add_attribute("action", "settle_auction".to_string()) - .add_attribute("settled_action_round", unsettled_auction.auction_round.to_string()) - .add_attribute("treasure_chest_address", treasure_chest_address.to_string()) - .add_attribute("current_action_round", current_auction_round.to_string()) - .add_attribute("new_subdenom", new_subdenom.to_string())) - } - // the contract did NOT win the auction - else { - // save the current auction details to the contract state, keeping the previous LP subdenom - UNSETTLED_AUCTION.save( - deps.storage, - &Auction { - basket: current_auction_round_response - .amount - .iter() - .map(|coin| Coin { - amount: Uint128::from_str(&coin.amount) - .expect("Failed to parse coin amount"), - denom: coin.denom.clone(), - }) - .collect(), - auction_round: current_auction_round, - lp_subdenom: unsettled_auction.lp_subdenom, - closing_time: current_auction_round_response.auction_closing_time(), - }, - )?; - - Ok(Response::default() - .add_attribute("action", "settle_auction".to_string()) - .add_attribute("settled_action_round", unsettled_auction.auction_round.to_string()) - .add_attribute("current_action_round", current_auction_round.to_string())) - } + Ok(Response::default() + .add_attribute("action", "settle_auction") + .add_messages(messages) + .add_attributes(attributes)) } diff --git a/contracts/injective-auction-pool/src/helpers.rs b/contracts/injective-auction-pool/src/helpers.rs index a95337f..8c8b430 100644 --- a/contracts/injective-auction-pool/src/helpers.rs +++ b/contracts/injective-auction-pool/src/helpers.rs @@ -1,7 +1,253 @@ -use crate::ContractError; -use cosmwasm_std::{Decimal, Deps, QueryRequest}; +use std::str::FromStr; + +use crate::{ + state::{Auction, BIDDING_BALANCE, CONFIG, TREASURE_CHEST_CONTRACTS, UNSETTLED_AUCTION}, + ContractError, +}; +use cosmwasm_std::{ + attr, instantiate2_address, to_json_binary, Addr, Attribute, BankMsg, Binary, CodeInfoResponse, + Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, OverflowError, QueryRequest, + Uint128, WasmMsg, +}; use injective_auction::auction::QueryCurrentAuctionBasketResponse; +/// Starts a new auction +pub(crate) fn new_auction_round( + deps: DepsMut, + env: &Env, + info: MessageInfo, + auction_winner: Option, + auction_winning_bid: Option, +) -> Result<(Vec, Vec), ContractError> { + let config = CONFIG.load(deps.storage)?; + + // fetch current auction details and save them in the contract state + let current_auction_round_response = query_current_auction(deps.as_ref())?; + + let current_auction_round = current_auction_round_response + .auction_round + .ok_or(ContractError::CurrentAuctionQueryError)?; + + let current_basket = current_auction_round_response + .amount + .iter() + .map(|coin| Coin { + amount: Uint128::from_str(&coin.amount).expect("Failed to parse coin amount"), + denom: coin.denom.clone(), + }) + .collect(); + + let unsettled_auction = UNSETTLED_AUCTION.may_load(deps.storage)?; + + let mut attributes = vec![]; + let mut messages = vec![]; + + match unsettled_auction { + Some(unsettled_auction) => { + let auction_winner = auction_winner.ok_or(ContractError::MissingAuctionWinner {})?; + let auction_winning_bid = + auction_winning_bid.ok_or(ContractError::MissingAuctionWinningBid {})?; + // the contract won the auction + // NOTE: this is assuming the bot is sending the correct data about the winner of the previous auction + // currently there's no way to query the auction module directly to get this information + if deps.api.addr_validate(&auction_winner)? == env.contract.address { + // update LP subdenom for the next auction round (increment by 1) + let new_subdenom = unsettled_auction.lp_subdenom.checked_add(1).ok_or( + ContractError::OverflowError(OverflowError { + operation: cosmwasm_std::OverflowOperation::Add, + operand1: unsettled_auction.lp_subdenom.to_string(), + operand2: 1.to_string(), + }), + )?; + + let unsettled_basket = unsettled_auction.basket; + let mut basket_fees = vec![]; + let mut basket_to_treasure_chest = vec![]; + + // add the unused bidding balance to the basket to be redeemed later + // TODO: should this be taxed though? if not, move after the for loop + let remaining_bidding_balance = + BIDDING_BALANCE.load(deps.storage)?.checked_sub(auction_winning_bid)?; + + if remaining_bidding_balance > Uint128::zero() { + basket_to_treasure_chest.push(Coin { + denom: config.native_denom.clone(), + amount: remaining_bidding_balance, + }); + } + + // split the basket, taking the rewards fees into account + for coin in unsettled_basket.iter() { + let fee = coin.amount * config.rewards_fee; + basket_fees.push(Coin { + denom: coin.denom.clone(), + amount: fee, + }); + basket_to_treasure_chest.push(Coin { + denom: coin.denom.clone(), + amount: coin.amount.checked_sub(fee)?, + }); + } + + // reset the bidding balance to 0 if we won, otherwise keep the balance for the next round + BIDDING_BALANCE.save(deps.storage, &Uint128::zero())?; + + let mut messages: Vec = vec![]; + + // transfer corresponding tokens to the rewards fee address + messages.push(CosmosMsg::Bank(BankMsg::Send { + to_address: config.rewards_fee_addr.to_string(), + amount: basket_fees, + })); + + // instantiate a treasury chest contract and get the future contract address + let creator = deps.api.addr_canonicalize(env.contract.address.as_str())?; + let code_id = config.treasury_chest_code_id; + + let CodeInfoResponse { + code_id: _, + creator: _, + checksum, + .. + } = deps.querier.query_wasm_code_info(code_id)?; + + let seed = format!( + "{}{}{}", + unsettled_auction.auction_round, + info.sender.into_string(), + env.block.height + ); + let salt = Binary::from(seed.as_bytes()); + + let treasure_chest_address = + Addr::unchecked(instantiate2_address(&checksum, &creator, &salt)?.to_string()); + + let denom = format!( + "factory/{}/auction.{}", + env.contract.address.to_string(), + unsettled_auction.lp_subdenom + ); + + messages.push(CosmosMsg::Wasm(WasmMsg::Instantiate2 { + admin: Some(env.contract.address.to_string()), + code_id, + label: format!( + "Treasure chest for auction round {}", + unsettled_auction.auction_round + ), + msg: to_json_binary(&treasurechest::chest::InstantiateMsg { + denom: config.native_denom.clone(), + owner: env.contract.address.to_string(), + notes: denom.clone(), + token_factory: config.token_factory_type.to_string(), + burn_it: Some(false), + })?, + funds: basket_to_treasure_chest, + salt, + })); + + TREASURE_CHEST_CONTRACTS.save( + deps.storage, + unsettled_auction.auction_round, + &treasure_chest_address, + )?; + + // transfer previous token factory's admin rights to the treasury chest contract + messages.push(config.token_factory_type.change_admin( + env.contract.address.clone(), + &denom, + treasure_chest_address.clone(), + )); + + // create a new denom for the current auction round + messages.push(config.token_factory_type.create_denom( + env.contract.address.clone(), + format!("auction.{}", new_subdenom).as_str(), + )); + + let basket = current_auction_round_response + .amount + .iter() + .map(|coin| Coin { + amount: Uint128::from_str(&coin.amount) + .expect("Failed to parse coin amount"), + denom: coin.denom.clone(), + }) + .collect(); + + UNSETTLED_AUCTION.save( + deps.storage, + &Auction { + basket, + auction_round: current_auction_round_response.auction_round(), + lp_subdenom: new_subdenom, + closing_time: current_auction_round_response.auction_closing_time(), + }, + )?; + attributes.push(attr( + "settled_auction_round", + unsettled_auction.auction_round.to_string(), + )); + attributes.push(attr("new_auction_round", current_auction_round.to_string())); + attributes.push(attr("treasure_chest_address", treasure_chest_address.to_string())); + attributes.push(attr("new_subdenom", format!("auction.{}", new_subdenom))); + + return Ok((messages, attributes)); + } + // the contract did NOT win the auction + else { + // save the current auction details to the contract state, keeping the previous LP subdenom + UNSETTLED_AUCTION.save( + deps.storage, + &Auction { + basket: current_auction_round_response + .amount + .iter() + .map(|coin| Coin { + amount: Uint128::from_str(&coin.amount) + .expect("Failed to parse coin amount"), + denom: coin.denom.clone(), + }) + .collect(), + auction_round: current_auction_round_response.auction_round(), + lp_subdenom: unsettled_auction.lp_subdenom, + closing_time: current_auction_round_response.auction_closing_time(), + }, + )?; + attributes.push(attr( + "settled_auction_round", + unsettled_auction.auction_round.to_string(), + )); + attributes.push(attr("new_auction_round", current_auction_round.to_string())); + return Ok((messages, attributes)); + } + }, + // should only happen on instantiation, initialize LP subdenom & bidding balance to 0 + None => { + UNSETTLED_AUCTION.save( + deps.storage, + &Auction { + basket: current_basket, + auction_round: current_auction_round_response.auction_round(), + lp_subdenom: 0, + closing_time: current_auction_round_response.auction_closing_time(), + }, + )?; + + BIDDING_BALANCE.save(deps.storage, &Uint128::zero())?; + + // create a new denom for the current auction round + messages.push( + config.token_factory_type.create_denom(env.contract.address.clone(), "auction.0"), + ); + + attributes.push(attr("new_auction_round", current_auction_round.to_string())); + attributes.push(attr("lp_subdenom", "auction.0")); + return Ok((messages, attributes)); + }, + } +} + /// Validates the rewards fee pub(crate) fn validate_percentage(percentage: Decimal) -> Result { if percentage > Decimal::percent(100) { diff --git a/contracts/injective-auction-pool/src/tests/unit_tests.rs b/contracts/injective-auction-pool/src/tests/unit_tests.rs index c82e951..c20c61a 100644 --- a/contracts/injective-auction-pool/src/tests/unit_tests.rs +++ b/contracts/injective-auction-pool/src/tests/unit_tests.rs @@ -6,6 +6,7 @@ use cosmwasm_std::{ ContractResult as CwContractResult, CosmosMsg, Decimal, Empty, Env, HexBinary, MemoryStorage, MessageInfo, OwnedDeps, Querier, QuerierResult, QueryRequest, Uint128, WasmMsg, WasmQuery, }; +use cw_ownable::Ownership; use injective_auction::auction::{Coin, MsgBid, QueryCurrentAuctionBasketResponse}; use injective_auction::auction_pool::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; use prost::Message; @@ -92,13 +93,14 @@ pub fn mock_deps_with_querier( } pub fn init() -> (OwnedDeps, Env) { - let info = mock_info("instantiator", &coins(100, "denom")); + let info = mock_info("instantiator", &coins(2, "native_denom")); let mut deps = mock_deps_with_querier(&info); let env = mock_env(); let msg = InstantiateMsg { owner: Some("owner".to_string()), native_denom: "native_denom".to_string(), + min_balance: Uint128::from(2u128), token_factory_type: TokenFactoryType::Injective, rewards_fee: Decimal::percent(10), rewards_fee_addr: "rewards_addr".to_string(), @@ -111,12 +113,16 @@ pub fn init() -> (OwnedDeps, Env) { assert_eq!( res.attributes, - vec![attr("action", "instantiate"), attr("auction_round", "1"), attr("lp_subdenom", "1"),] + vec![ + attr("action", "instantiate"), + attr("new_auction_round", "1"), + attr("lp_subdenom", "auction.0"), + ] ); assert_eq!( res.messages[0].msg, - TokenFactoryType::Injective.create_denom(env.contract.address.clone(), "1") + TokenFactoryType::Injective.create_denom(env.contract.address.clone(), "auction.0") ); assert_eq!(BIDDING_BALANCE.load(&deps.storage).unwrap(), Uint128::zero()); @@ -131,7 +137,6 @@ fn update_config() { // update config as non-owner should fail let info = mock_info("not_owner", &[]); let msg = ExecuteMsg::UpdateConfig { - owner: None, rewards_fee: None, rewards_fee_addr: None, whitelist_addresses: None, @@ -139,12 +144,11 @@ fn update_config() { min_return: None, }; let res = execute(deps.as_mut().branch(), env.clone(), info, msg.clone()).unwrap_err(); - assert_eq!(res, ContractError::Unauthorized {}); + assert_eq!(res, ContractError::Ownership(cw_ownable::OwnershipError::NotOwner)); // update some of the config fields as owner should work let info = mock_info("owner", &[]); let msg = ExecuteMsg::UpdateConfig { - owner: Some("new_owner".to_string()), rewards_fee: Some(Decimal::percent(20)), rewards_fee_addr: Some("new_rewards_addr".to_string()), whitelist_addresses: Some(vec!["new_bot".to_string()]), @@ -156,7 +160,6 @@ fn update_config() { res.attributes, vec![ attr("action", "update_config"), - attr("owner", "new_owner"), attr("native_denom", "native_denom"), attr("token_factory_type", "Injective"), attr("rewards_fee", "0.2"), @@ -172,7 +175,6 @@ fn update_config() { let msg = QueryMsg::Config {}; let res: ConfigResponse = from_json(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); let config = res.config; - assert_eq!(config.owner, Addr::unchecked("new_owner")); assert_eq!(config.rewards_fee, Decimal::percent(20)); assert_eq!(config.rewards_fee_addr, "new_rewards_addr".to_string()); assert_eq!(config.whitelisted_addresses, vec!["new_bot".to_string()]); @@ -180,6 +182,43 @@ fn update_config() { assert_eq!(config.min_return, Decimal::percent(10)); } +#[test] +fn update_ownership() { + let (mut deps, env) = init(); + + // update ownership as non-owner should fail + let info = mock_info("not_owner", &[]); + let msg = ExecuteMsg::UpdateOwnership(cw_ownable::Action::TransferOwnership { + new_owner: "new_owner".to_string(), + expiry: None, + }); + let res = execute(deps.as_mut().branch(), env.clone(), info, msg.clone()).unwrap_err(); + assert_eq!(res, ContractError::Ownership(cw_ownable::OwnershipError::NotOwner)); + + // update ownership as owner should work + let info = mock_info("owner", &[]); + let msg = ExecuteMsg::UpdateOwnership(cw_ownable::Action::TransferOwnership { + new_owner: "new_owner".to_string(), + expiry: None, + }); + let _ = execute(deps.as_mut().branch(), env.clone(), info, msg.clone()).unwrap(); + + // ownership should not be updated until accepted + let msg = QueryMsg::Ownership {}; + let res: Ownership = from_json(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); + assert_eq!(res.owner.unwrap(), "owner"); + + // accept ownership as new_owner should work + let info = mock_info("new_owner", &[]); + let msg = ExecuteMsg::UpdateOwnership(cw_ownable::Action::AcceptOwnership {}); + let _ = execute(deps.as_mut().branch(), env.clone(), info, msg.clone()).unwrap(); + + // query the ownership to check if it was updated + let msg = QueryMsg::Ownership {}; + let res: Ownership = from_json(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); + assert_eq!(res.owner.unwrap(), "new_owner"); +} + #[test] pub fn join_pool_works() { let (mut deps, env) = init(); @@ -194,8 +233,11 @@ pub fn join_pool_works() { // contracts mints 100 lp tokens to itself. Subdenom is 1 as it's the first auction assert_eq!( res.messages[0].msg, - TokenFactoryType::Injective - .mint(env.contract.address.clone(), "1", Uint128::from(100u128),) + TokenFactoryType::Injective.mint( + env.contract.address.clone(), + "auction.0", + Uint128::from(100u128), + ) ); // contract sends 100 lp tokens to the user @@ -203,7 +245,7 @@ pub fn join_pool_works() { res.messages[1].msg, BankMsg::Send { to_address: "robinho".to_string(), - amount: coins(100, format!("factory/{}/{}", MOCK_CONTRACT_ADDR, 1)), + amount: coins(100, format!("factory/{}/{}", MOCK_CONTRACT_ADDR, "auction.0")), } .into() ); @@ -292,7 +334,8 @@ fn exit_pool_works() { }; let _ = execute(deps.as_mut().branch(), env.clone(), info, msg).unwrap(); - let info = mock_info("robinho", &coins(100, format!("factory/{}/1", env.contract.address))); + let info = + mock_info("robinho", &coins(100, format!("factory/{}/auction.0", env.contract.address))); let msg = ExecuteMsg::ExitPool {}; let res = execute(deps.as_mut().branch(), env.clone(), info, msg).unwrap(); @@ -302,7 +345,7 @@ fn exit_pool_works() { res.messages[0].msg, TokenFactoryType::Injective.burn( Addr::unchecked(MOCK_CONTRACT_ADDR), - format!("factory/{}/1", MOCK_CONTRACT_ADDR).as_str(), + format!("factory/{}/auction.0", MOCK_CONTRACT_ADDR).as_str(), Uint128::from(100u128), ) ); @@ -338,7 +381,7 @@ fn exit_pool_fails() { assert_eq!( res, ContractError::PaymentError(cw_utils::PaymentError::MissingDenom(format!( - "factory/{MOCK_CONTRACT_ADDR}/1", + "factory/{MOCK_CONTRACT_ADDR}/auction.0", ))) ); @@ -348,7 +391,8 @@ fn exit_pool_fails() { assert_eq!(res, ContractError::PaymentError(cw_utils::PaymentError::NoFunds {})); // exit pool in T-1 day should fail - let info = mock_info("robinho", &coins(100, format!("factory/{}/1", env.contract.address))); + let info = + mock_info("robinho", &coins(100, format!("factory/{}/auction.0", env.contract.address))); env.block.time = env.block.time.plus_seconds(6 * 86_400 + 1); let res = execute(deps.as_mut().branch(), env.clone(), info.clone(), msg.clone()).unwrap_err(); assert_eq!(res, ContractError::PooledAuctionLocked {}); @@ -361,7 +405,7 @@ fn exit_pool_fails() { res.messages[0].msg, TokenFactoryType::Injective.burn( Addr::unchecked(MOCK_CONTRACT_ADDR), - format!("factory/{}/1", MOCK_CONTRACT_ADDR).as_str(), + format!("factory/{}/auction.0", MOCK_CONTRACT_ADDR).as_str(), Uint128::from(100u128), ) ); @@ -544,8 +588,8 @@ fn try_bid_fails() { // res.attributes, // vec![ // attr("action", "settle_auction"), -// attr("settled_action_round", "0"), -// attr("current_action_round", "1"), +// attr("settled_auction_round", "0"), +// attr("new_auction_round", "1"), // ] // ); @@ -553,7 +597,7 @@ fn try_bid_fails() { // assert_eq!(unsettled_auction.auction_round, 1); // assert_eq!(unsettled_auction.basket, vec![coin(10_000, "uatom")]); // assert_eq!(unsettled_auction.closing_time, 1_571_797_419 + 7 * 86_400); -// assert_eq!(unsettled_auction.lp_subdenom, 1); +// assert_eq!(unsettled_auction.lp_subdenom, 0); // } // #[test] @@ -571,6 +615,7 @@ fn try_bid_fails() { // // mock the auction round to be 0 so the contract thinks the auction round is over // let mut unsettled_auction = UNSETTLED_AUCTION.load(deps.as_ref().storage).unwrap(); // unsettled_auction.auction_round = 0; +// unsettled_auction.lp_subdenom = 1; // UNSETTLED_AUCTION.save(deps.as_mut().storage, &unsettled_auction).unwrap(); // env.block.time = env.block.time.plus_days(7); @@ -641,13 +686,13 @@ fn try_bid_fails() { // res.attributes, // vec![ // attr("action", "settle_auction"), -// attr("settled_action_round", "0"), +// attr("settled_auction_round", "0"), +// attr("new_auction_round", "1"), // attr( // "treasure_chest_address", // // this is a mock address, as the checksum was invented // "ED9963158CC851609F6BCFE30C3256F3471F11E3087F6DB5244B1FE5659757C4" // ), -// attr("current_action_round", "1"), // attr("new_subdenom", "2"), // ] // ); diff --git a/packages/injective_auction/Cargo.toml b/packages/injective_auction/Cargo.toml index b344d48..6d7c7be 100644 --- a/packages/injective_auction/Cargo.toml +++ b/packages/injective_auction/Cargo.toml @@ -21,6 +21,7 @@ prost = { workspace = true } prost-types = { workspace = true } osmosis-std-derive = { workspace = true } treasurechest = { workspace = true } +cw-ownable = { workspace = true } [dev-dependencies] protobuf = { workspace = true } diff --git a/packages/injective_auction/src/auction_pool.rs b/packages/injective_auction/src/auction_pool.rs index b9b6cfc..098a34c 100644 --- a/packages/injective_auction/src/auction_pool.rs +++ b/packages/injective_auction/src/auction_pool.rs @@ -1,11 +1,13 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Decimal, Uint128}; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; use treasurechest::tf::tokenfactory::TokenFactoryType; #[cw_serde] pub struct InstantiateMsg { pub owner: Option, pub native_denom: String, + pub min_balance: Uint128, pub token_factory_type: TokenFactoryType, pub rewards_fee: Decimal, pub rewards_fee_addr: String, @@ -15,11 +17,10 @@ pub struct InstantiateMsg { pub min_return: Decimal, } +#[cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { UpdateConfig { - /// New owner of the contract - owner: Option, /// Percentage of the rewards that the rewards fee address will take. Value is between 0 and 1 rewards_fee: Option, /// Address to receive the rewards fee @@ -58,6 +59,7 @@ pub enum ExecuteMsg { }, } +#[cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { @@ -90,10 +92,10 @@ pub struct BiddingBalanceResponse { #[cw_serde] /// Config of the contract pub struct Config { - /// Owner of the contract - pub owner: Addr, /// Contract native denom pub native_denom: String, + /// Minimum balance to keep in the contract to create a new denoms + pub min_balance: Uint128, /// Token Factory Type for the contract pub token_factory_type: TokenFactoryType, /// Percentage of the rewards that the rewards fee address will take. Value is between 0 and 1