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
80 changes: 72 additions & 8 deletions contracts/stake-vault/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![no_std]
use soroban_sdk::{contract, contractevent, contractimpl, token, Address, BytesN, Env};
use soroban_sdk::{contract, contractevent, contractimpl, token, Address, BytesN, Env, Vec};

pub mod types;
use types::{DataKey, StakeInfo};
use types::{default_multiplier_tiers, DataKey, MultiplierTier, StakeInfo};

#[contract]
pub struct StakeVault;
Expand Down Expand Up @@ -31,13 +31,38 @@ pub struct Unstaked {
pub amount: i128,
}

#[contractevent]
pub struct MultiplierTiersUpdated {
#[topic]
pub admin: Address,
pub tiers: Vec<MultiplierTier>,
}

#[contractevent]
pub struct ContractUpgraded {
#[topic]
pub admin: Address,
pub new_wasm_hash: BytesN<32>,
}

fn validate_tiers(tiers: &Vec<MultiplierTier>) {
let mut prev_min_stake: Option<i128> = None;
for tier in tiers.iter() {
if tier.min_stake <= 0 {
panic!("Tier min_stake must be positive");
}
if tier.multiplier == 0 {
panic!("Tier multiplier must be non-zero");
}
if let Some(prev) = prev_min_stake {
if tier.min_stake >= prev {
panic!("Tier min_stake must be strictly descending");
}
}
prev_min_stake = Some(tier.min_stake);
}
}

#[contractimpl]
impl StakeVault {
pub fn initialize(env: Env, admin: Address, token: Address) {
Expand All @@ -50,9 +75,42 @@ impl StakeVault {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Token, &token);

let default_tiers = default_multiplier_tiers(&env);
env.storage()
.instance()
.set(&DataKey::MultiplierTiers, &default_tiers);

StakeVaultInitialized { admin, token }.publish(&env);
}

pub fn set_multiplier_tiers(env: Env, admin: Address, tiers: Vec<MultiplierTier>) {
admin.require_auth();

let stored_admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.expect("Not initialized");
if admin != stored_admin {
panic!("Unauthorized");
}

validate_tiers(&tiers);

env.storage()
.instance()
.set(&DataKey::MultiplierTiers, &tiers);

MultiplierTiersUpdated { admin, tiers }.publish(&env);
}

pub fn get_multiplier_tiers(env: Env) -> Vec<MultiplierTier> {
env.storage()
.instance()
.get(&DataKey::MultiplierTiers)
.unwrap_or_else(|| default_multiplier_tiers(&env))
}

pub fn stake(env: Env, user: Address, amount: i128) {
user.require_auth();

Expand Down Expand Up @@ -144,13 +202,19 @@ impl StakeVault {
lock_timestamp: 0,
});

if stake_info.amount >= 500 {
200
} else if stake_info.amount >= 100 {
120
} else {
100
let tiers: Vec<MultiplierTier> = env
.storage()
.instance()
.get(&DataKey::MultiplierTiers)
.unwrap_or_else(|| default_multiplier_tiers(&env));

for tier in tiers.iter() {
if stake_info.amount >= tier.min_stake {
return tier.multiplier;
}
}

100
}

pub fn upgrade_contract(env: Env, admin: Address, new_wasm_hash: BytesN<32>) {
Expand Down
151 changes: 149 additions & 2 deletions contracts/stake-vault/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

use soroban_sdk::{
testutils::{Address as _, Ledger},
token, Address, Env,
token, Address, Env, Vec,
};

use crate::{
types::{DataKey, StakeInfo},
types::{DataKey, MultiplierTier, StakeInfo},
StakeVault, StakeVaultClient,
};

Expand Down Expand Up @@ -205,3 +205,150 @@ fn test_get_multiplier() {
});
assert_eq!(client.get_multiplier(&user), 200);
}

#[test]
fn test_set_multiplier_tiers_success() {
let (env, client) = setup();
let admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());
let user = Address::generate(&env);

client.initialize(&admin, &token_id.address());

let new_tiers = Vec::from_array(
&env,
[
MultiplierTier {
min_stake: 1000,
multiplier: 300,
},
MultiplierTier {
min_stake: 200,
multiplier: 150,
},
],
);

client.set_multiplier_tiers(&admin, &new_tiers);
assert_eq!(client.get_multiplier_tiers(), new_tiers);

env.as_contract(&client.address, || {
env.storage().persistent().set(
&DataKey::UserStake(user.clone()),
&StakeInfo {
amount: 150,
lock_timestamp: 0,
},
);
});
assert_eq!(client.get_multiplier(&user), 100);

env.as_contract(&client.address, || {
env.storage().persistent().set(
&DataKey::UserStake(user.clone()),
&StakeInfo {
amount: 200,
lock_timestamp: 0,
},
);
});
assert_eq!(client.get_multiplier(&user), 150);

env.as_contract(&client.address, || {
env.storage().persistent().set(
&DataKey::UserStake(user.clone()),
&StakeInfo {
amount: 1000,
lock_timestamp: 0,
},
);
});
assert_eq!(client.get_multiplier(&user), 300);
}

#[test]
#[should_panic(expected = "Unauthorized")]
fn test_set_multiplier_tiers_unauthorized() {
let (env, client) = setup();
let admin = Address::generate(&env);
let non_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());

let new_tiers = Vec::from_array(
&env,
[MultiplierTier {
min_stake: 500,
multiplier: 200,
}],
);

client.set_multiplier_tiers(&non_admin, &new_tiers);
}

#[test]
#[should_panic(expected = "Tier min_stake must be strictly descending")]
fn test_set_multiplier_tiers_invalid_order() {
let (env, client) = setup();
let admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());

let invalid_tiers = Vec::from_array(
&env,
[
MultiplierTier {
min_stake: 100,
multiplier: 120,
},
MultiplierTier {
min_stake: 500,
multiplier: 200,
},
],
);

client.set_multiplier_tiers(&admin, &invalid_tiers);
}

#[test]
#[should_panic(expected = "Tier min_stake must be positive")]
fn test_set_multiplier_tiers_invalid_min_stake() {
let (env, client) = setup();
let admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());

let invalid_tiers = Vec::from_array(
&env,
[MultiplierTier {
min_stake: 0,
multiplier: 150,
}],
);

client.set_multiplier_tiers(&admin, &invalid_tiers);
}

#[test]
#[should_panic(expected = "Tier multiplier must be non-zero")]
fn test_set_multiplier_tiers_invalid_multiplier() {
let (env, client) = setup();
let admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(admin.clone());

client.initialize(&admin, &token_id.address());

let invalid_tiers = Vec::from_array(
&env,
[MultiplierTier {
min_stake: 500,
multiplier: 0,
}],
);

client.set_multiplier_tiers(&admin, &invalid_tiers);
}
26 changes: 25 additions & 1 deletion contracts/stake-vault/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{contracttype, Address};
use soroban_sdk::{contracttype, Address, Env, Vec};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
Expand All @@ -7,10 +7,34 @@ pub struct StakeInfo {
pub lock_timestamp: u64,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiplierTier {
pub min_stake: i128,
pub multiplier: u32,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DataKey {
Admin,
Token,
UserStake(Address),
MultiplierTiers,
}

pub fn default_multiplier_tiers(env: &Env) -> Vec<MultiplierTier> {
Vec::from_array(
env,
[
MultiplierTier {
min_stake: 500,
multiplier: 200,
},
MultiplierTier {
min_stake: 100,
multiplier: 120,
},
],
)
}
Loading