Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion chance-staking/contracts/staking-hub/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ pub fn instantiate(
base_yield_bps: msg.base_yield_bps,
regular_pool_bps: msg.regular_pool_bps,
big_pool_bps: msg.big_pool_bps,
min_epochs_regular: msg.min_epochs_regular,
min_epochs_big: msg.min_epochs_big,
};

CONFIG.save(deps.storage, &config)?;
Expand Down Expand Up @@ -120,7 +122,18 @@ pub fn execute(
admin,
operator,
protocol_fee_bps,
} => execute::update_config(deps, env, info, admin, operator, protocol_fee_bps),
min_epochs_regular,
min_epochs_big,
} => execute::update_config(
deps,
env,
info,
admin,
operator,
protocol_fee_bps,
min_epochs_regular,
min_epochs_big,
),
ExecuteMsg::UpdateValidators { add, remove } => {
execute::update_validators(deps, env, info, add, remove)
}
Expand All @@ -138,6 +151,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
start_after,
limit,
} => query::query_unstake_requests(deps, address, start_after, limit),
QueryMsg::StakerInfo { address } => query::query_staker_info(deps, address),
}
}

Expand All @@ -163,6 +177,8 @@ mod tests {
regular_pool_bps: 7000,
big_pool_bps: 2000,
csinj_subdenom: "csINJ".to_string(),
min_epochs_regular: 0,
min_epochs_big: 0,
}
}

Expand Down Expand Up @@ -651,4 +667,91 @@ mod tests {
.unwrap_err();
assert!(matches!(err, ContractError::NoValidators));
}

#[test]
fn test_stake_records_and_resets_epoch() {
use crate::state::{EPOCH_STATE, USER_STAKE_EPOCH};

let mut deps = mock_dependencies();
setup_contract(deps.as_mut());

let user1 = deps.api.addr_make("user1");
let info = message_info(&user1, &coins(100_000_000, "inj"));
execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Stake {}).unwrap();

// Verify stake epoch recorded as epoch 1
let addr = deps.api.addr_make("user1");
let stake_epoch = USER_STAKE_EPOCH.load(deps.as_ref().storage, &addr).unwrap();
assert_eq!(stake_epoch, 1);

// Simulate epoch advancing to 5
let mut epoch_state = EPOCH_STATE.load(deps.as_ref().storage).unwrap();
epoch_state.current_epoch = 5;
EPOCH_STATE
.save(deps.as_mut().storage, &epoch_state)
.unwrap();

// Second stake should reset epoch to 5
let user1 = deps.api.addr_make("user1");
let info = message_info(&user1, &coins(50_000_000, "inj"));
execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Stake {}).unwrap();

let addr = deps.api.addr_make("user1");
let stake_epoch = USER_STAKE_EPOCH.load(deps.as_ref().storage, &addr).unwrap();
assert_eq!(stake_epoch, 5); // reset to current epoch
}

#[test]
fn test_staker_info_query() {
use crate::msg::StakerInfoResponse;
use cosmwasm_std::from_json;

let mut deps = mock_dependencies();
setup_contract(deps.as_mut());

// Query before staking — should return None
let user1 = deps.api.addr_make("user1");
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::StakerInfo {
address: user1.to_string(),
},
)
.unwrap();
let info: StakerInfoResponse = from_json(res).unwrap();
assert_eq!(info.stake_epoch, None);

// Stake
let user1 = deps.api.addr_make("user1");
let msg_info = message_info(&user1, &coins(100_000_000, "inj"));
execute(deps.as_mut(), mock_env(), msg_info, ExecuteMsg::Stake {}).unwrap();

// Query after staking — should return epoch 1
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::StakerInfo {
address: user1.to_string(),
},
)
.unwrap();
let info: StakerInfoResponse = from_json(res).unwrap();
assert_eq!(info.stake_epoch, Some(1));
}

#[test]
fn test_min_epochs_in_config() {
let mut deps = mock_dependencies();
let mut msg = default_instantiate_msg();
msg.min_epochs_regular = 3;
msg.min_epochs_big = 7;
let admin = deps.api.addr_make("admin");
let info = message_info(&admin, &[]);
instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();

let config = CONFIG.load(deps.as_ref().storage).unwrap();
assert_eq!(config.min_epochs_regular, 3);
assert_eq!(config.min_epochs_big, 7);
}
}
15 changes: 14 additions & 1 deletion chance-staking/contracts/staking-hub/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::error::ContractError;
use crate::msg::DistributorExecuteMsg;
use crate::state::{
UnstakeRequest, CONFIG, EPOCH_STATE, EXCHANGE_RATE, NEXT_UNSTAKE_ID, PENDING_UNSTAKE_TOTAL,
TOTAL_CSINJ_SUPPLY, TOTAL_INJ_BACKING, UNSTAKE_REQUESTS,
TOTAL_CSINJ_SUPPLY, TOTAL_INJ_BACKING, UNSTAKE_REQUESTS, USER_STAKE_EPOCH,
};

/// 10^18 — Decimal's internal scaling factor, used for overflow-safe rate arithmetic.
Expand Down Expand Up @@ -82,6 +82,10 @@ pub fn stake(
epoch_state.total_staked = new_backing;
EPOCH_STATE.save(deps.storage, &epoch_state)?;

// Record the epoch of this stake (resets on every stake so that newly added
// funds must also satisfy the min_epochs eligibility requirement)
USER_STAKE_EPOCH.save(deps.storage, &info.sender, &epoch_state.current_epoch)?;

// Mint csINJ via Token Factory
let mint_msg = create_mint_tokens_msg(
env.contract.address.clone(),
Expand Down Expand Up @@ -499,13 +503,16 @@ pub fn take_snapshot(
}

/// Update contract configuration. Admin only.
#[allow(clippy::too_many_arguments)]
pub fn update_config(
deps: DepsMut,
_env: Env,
info: MessageInfo,
admin: Option<String>,
operator: Option<String>,
protocol_fee_bps: Option<u16>,
min_epochs_regular: Option<u64>,
min_epochs_big: Option<u64>,
) -> Result<ContractResponse, ContractError> {
let mut config = CONFIG.load(deps.storage)?;

Expand All @@ -530,6 +537,12 @@ pub fn update_config(
}
config.protocol_fee_bps = new_fee;
}
if let Some(val) = min_epochs_regular {
config.min_epochs_regular = val;
}
if let Some(val) = min_epochs_big {
config.min_epochs_big = val;
}

CONFIG.save(deps.storage, &config)?;

Expand Down
15 changes: 15 additions & 0 deletions chance-staking/contracts/staking-hub/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ pub struct InstantiateMsg {
pub big_pool_bps: u16,
/// Subdenom for Token Factory, e.g. "csINJ"
pub csinj_subdenom: String,
/// Minimum epochs staked to be eligible for regular draws
pub min_epochs_regular: u64,
/// Minimum epochs staked to be eligible for big draws
pub min_epochs_big: u64,
}

#[cw_serde]
Expand Down Expand Up @@ -45,6 +49,8 @@ pub enum ExecuteMsg {
admin: Option<String>,
operator: Option<String>,
protocol_fee_bps: Option<u16>,
min_epochs_regular: Option<u64>,
min_epochs_big: Option<u64>,
},
/// Update validator set. Admin only.
UpdateValidators {
Expand Down Expand Up @@ -81,6 +87,8 @@ pub enum QueryMsg {
start_after: Option<u64>,
limit: Option<u32>,
},
#[returns(StakerInfoResponse)]
StakerInfo { address: String },
}

#[cw_serde]
Expand All @@ -95,3 +103,10 @@ pub struct UnstakeRequestEntry {
pub id: u64,
pub request: UnstakeRequest,
}

#[cw_serde]
pub struct StakerInfoResponse {
pub address: String,
/// The epoch of this user's most recent stake, or None if they have never staked.
pub stake_epoch: Option<u64>,
}
12 changes: 11 additions & 1 deletion chance-staking/contracts/staking-hub/src/query.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use cosmwasm_std::{to_json_binary, Binary, Deps, Order, StdResult};
use cw_storage_plus::Bound;

use crate::msg::{ExchangeRateResponse, UnstakeRequestEntry};
use crate::msg::{ExchangeRateResponse, StakerInfoResponse, UnstakeRequestEntry};
use crate::state::{
CONFIG, EPOCH_STATE, EXCHANGE_RATE, TOTAL_CSINJ_SUPPLY, TOTAL_INJ_BACKING, UNSTAKE_REQUESTS,
USER_STAKE_EPOCH,
};

pub fn query_config(deps: Deps) -> StdResult<Binary> {
Expand Down Expand Up @@ -48,3 +49,12 @@ pub fn query_unstake_requests(

to_json_binary(&entries)
}

pub fn query_staker_info(deps: Deps, address: String) -> StdResult<Binary> {
let addr = deps.api.addr_validate(&address)?;
let stake_epoch = USER_STAKE_EPOCH.may_load(deps.storage, &addr)?;
to_json_binary(&StakerInfoResponse {
address,
stake_epoch,
})
}
7 changes: 7 additions & 0 deletions chance-staking/contracts/staking-hub/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub const NEXT_UNSTAKE_ID: Map<&Addr, u64> = Map::new("next_unstake_id");
/// Updated on unstake (increment) and claim_unstaked (decrement) to avoid
/// iterating all requests on every distribute_rewards() call.
pub const PENDING_UNSTAKE_TOTAL: Item<Uint128> = Item::new("pending_unstake");
/// Tracks the epoch of the user's most recent stake. Resets on every stake
/// so newly added funds must also satisfy the min_epochs eligibility requirement.
pub const USER_STAKE_EPOCH: Map<&Addr, u64> = Map::new("user_stake_epoch");

#[cw_serde]
pub struct Config {
Expand All @@ -33,6 +36,10 @@ pub struct Config {
pub regular_pool_bps: u16,
/// Big draw pool in basis points (2000 = 20%)
pub big_pool_bps: u16,
/// Minimum epochs a user must have been staking to be eligible for regular draws
pub min_epochs_regular: u64,
/// Minimum epochs a user must have been staking to be eligible for big draws
pub min_epochs_big: u64,
}

#[cw_serde]
Expand Down
2 changes: 1 addition & 1 deletion chance-staking/packages/chance-staking-common/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Uint128;

/// The type of draw: regular (weighted by csINJ balance) or big (equal weight monthly).
/// The type of draw: regular or big. Both are weighted by csINJ balance.
#[cw_serde]
pub enum DrawType {
Regular,
Expand Down
6 changes: 5 additions & 1 deletion chance-staking/scripts/deploy_testnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ BASE_YIELD_BPS=500
REGULAR_POOL_BPS=7000
BIG_POOL_BPS=2000
CSINJ_SUBDENOM="csINJ"
MIN_EPOCHS_REGULAR=1
MIN_EPOCHS_BIG=1

# -- Wasm artifacts --
DRAND_ORACLE_WASM="./artifacts/chance_drand_oracle.wasm"
Expand Down Expand Up @@ -295,7 +297,9 @@ STAKING_HUB_INIT_MSG=$(cat <<EOF
"base_yield_bps": $BASE_YIELD_BPS,
"regular_pool_bps": $REGULAR_POOL_BPS,
"big_pool_bps": $BIG_POOL_BPS,
"csinj_subdenom": "$CSINJ_SUBDENOM"
"csinj_subdenom": "$CSINJ_SUBDENOM",
"min_epochs_regular": $MIN_EPOCHS_REGULAR,
"min_epochs_big": $MIN_EPOCHS_BIG
}
EOF
)
Expand Down
6 changes: 5 additions & 1 deletion chance-staking/scripts/deploy_testnet_fast.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ BASE_YIELD_BPS=500
REGULAR_POOL_BPS=7000
BIG_POOL_BPS=2000
CSINJ_SUBDENOM="csINJ"
MIN_EPOCHS_REGULAR=1
MIN_EPOCHS_BIG=1

# -- Wasm artifacts --
DRAND_ORACLE_WASM="./artifacts/chance_drand_oracle.wasm"
Expand Down Expand Up @@ -304,7 +306,9 @@ STAKING_HUB_INIT_MSG=$(cat <<EOF
"base_yield_bps": $BASE_YIELD_BPS,
"regular_pool_bps": $REGULAR_POOL_BPS,
"big_pool_bps": $BIG_POOL_BPS,
"csinj_subdenom": "$CSINJ_SUBDENOM"
"csinj_subdenom": "$CSINJ_SUBDENOM",
"min_epochs_regular": $MIN_EPOCHS_REGULAR,
"min_epochs_big": $MIN_EPOCHS_BIG
}
EOF
)
Expand Down
2 changes: 2 additions & 0 deletions chance-staking/tests/integration/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ fn hub_instantiate_msg() -> chance_staking_hub::msg::InstantiateMsg {
regular_pool_bps: 7000,
big_pool_bps: 2000,
csinj_subdenom: "csINJ".to_string(),
min_epochs_regular: 0,
min_epochs_big: 0,
}
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import StakingPanel from './components/StakingPanel'
import PortfolioSection from './components/PortfolioSection'
import DrawsSection from './components/DrawsSection'
import DrawDetail from './components/DrawDetail'
import RewardsCalculator from './components/RewardsCalculator'
import HowItWorks from './components/HowItWorks'
import Footer from './components/Footer'
import ToastContainer from './components/Toast'
Expand Down Expand Up @@ -88,6 +89,7 @@ function App() {
<main>
<HeroSection />
<StakingPanel />
<RewardsCalculator />
{isConnected && <PortfolioSection />}
<DrawsSection />
<HowItWorks />
Expand Down
Loading