Skip to content
Open
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ members = [
"contracts/course-registry",
"contracts/reward-pool",
"contracts/quest-engine",
"contracts/governance",
"contracts/stake-vault"
"contracts/badge-nft",
"contracts/governance"
]
Expand Down
7 changes: 7 additions & 0 deletions contracts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion contracts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
[workspace]
members = ["course-registry", "quest-engine", "reward-pool", "badge-nft", "governance"]
members = [
"course-registry",
"quest-engine",
"reward-pool",
"badge-nft",
"governance",
"stake-vault",
]
resolver = "2"

[workspace.dependencies]
Expand Down
18 changes: 18 additions & 0 deletions contracts/stake-vault/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "stake-vault"
version = "0.1.0"
edition = "2021"
authors = ["Learnault Team"]
description = "Stake vault contract for the Learnault platform"
license = "MIT"
repository = "https://github.com/learnault/learnault-contracts"

[lib]
crate-type = ["lib", "cdylib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
127 changes: 127 additions & 0 deletions contracts/stake-vault/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#![no_std]
use soroban_sdk::{contract, contractevent, contractimpl, token, Address, Env};

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

#[contract]
pub struct StakeVault;

#[contractevent]
pub struct StakeVaultInitialized {
#[topic]
pub admin: Address,
#[topic]
pub token: Address,
}

#[contractevent]
pub struct Staked {
#[topic]
pub user: Address,
pub amount: i128,
pub total_staked: i128,
pub lock_timestamp: u64,
}

#[contractevent]
pub struct Unstaked {
#[topic]
pub user: Address,
pub amount: i128,
}

#[contractimpl]
impl StakeVault {
pub fn initialize(env: Env, admin: Address, token: Address) {
if env.storage().instance().has(&DataKey::Admin) {
panic!("Already initialized");
}

admin.require_auth();

env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Token, &token);

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

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

if amount <= 0 {
panic!("Amount must be positive");
}

let token_id: Address = env
.storage()
.instance()
.get(&DataKey::Token)
.expect("Not initialized");
let token_client = token::Client::new(&env, &token_id);

token_client.transfer(&user, env.current_contract_address(), &amount);

let now = env.ledger().timestamp();

let mut stake_info: StakeInfo = env
.storage()
.persistent()
.get(&DataKey::UserStake(user.clone()))
.unwrap_or(StakeInfo {
amount: 0,
lock_timestamp: now,
});

stake_info.amount += amount;
stake_info.lock_timestamp = now;

env.storage()
.persistent()
.set(&DataKey::UserStake(user.clone()), &stake_info);

Staked {
user,
amount,
total_staked: stake_info.amount,
lock_timestamp: stake_info.lock_timestamp,
}
.publish(&env);
}

pub fn unstake(env: Env, user: Address) {
user.require_auth();

let stake_info: StakeInfo = env
.storage()
.persistent()
.get(&DataKey::UserStake(user.clone()))
.expect("No stake found");

let lock_period: u64 = 604800;
if env.ledger().timestamp() < stake_info.lock_timestamp + lock_period {
panic!("Lock period active");
}

let token_id: Address = env
.storage()
.instance()
.get(&DataKey::Token)
.expect("Not initialized");
let token_client = token::Client::new(&env, &token_id);

token_client.transfer(&env.current_contract_address(), &user, &stake_info.amount);

env.storage()
.persistent()
.remove(&DataKey::UserStake(user.clone()));

Unstaked {
user,
amount: stake_info.amount,
}
.publish(&env);
}
}

mod test;
141 changes: 141 additions & 0 deletions contracts/stake-vault/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#![cfg(test)]

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

use crate::{StakeVault, StakeVaultClient};

fn setup() -> (Env, StakeVaultClient<'static>) {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register(StakeVault, ());
let client = StakeVaultClient::new(&env, &contract_id);
(env, client)
}

#[test]
fn test_stake_transfers_and_accumulates() {
let (env, client) = setup();

let admin = Address::generate(&env);
let user = Address::generate(&env);

let token_id = env.register_stellar_asset_contract_v2(admin.clone());
let token_client = token::StellarAssetClient::new(&env, &token_id.address());

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

token_client.mint(&user, &1000);

env.ledger().set_timestamp(1_000_000);
client.stake(&user, &100);

assert_eq!(token_client.balance(&user), 900);
assert_eq!(token_client.balance(&client.address), 100);

env.ledger().set_timestamp(1_000_100);
client.stake(&user, &50);

assert_eq!(token_client.balance(&user), 850);
assert_eq!(token_client.balance(&client.address), 150);

env.ledger().set_timestamp(1_000_100 + 604800);
client.unstake(&user);

assert_eq!(token_client.balance(&user), 1000);
assert_eq!(token_client.balance(&client.address), 0);
}

#[test]
#[should_panic(expected = "Lock period active")]
fn test_stake_resets_lock_timestamp() {
let (env, client) = setup();

let admin = Address::generate(&env);
let user = Address::generate(&env);

let token_id = env.register_stellar_asset_contract_v2(admin.clone());
let token_client = token::StellarAssetClient::new(&env, &token_id.address());

client.initialize(&admin, &token_id.address());
token_client.mint(&user, &1000);

env.ledger().set_timestamp(10_000);
client.stake(&user, &100);

env.ledger().set_timestamp(10_100);
client.stake(&user, &50);

env.ledger().set_timestamp(10_000 + 604800);
client.unstake(&user);
}

#[test]
#[should_panic(expected = "Lock period active")]
fn test_unstake_panics_when_lock_period_active() {
let (env, client) = setup();

let admin = Address::generate(&env);
let user = Address::generate(&env);

let token_id = env.register_stellar_asset_contract_v2(admin.clone());
let token_client = token::StellarAssetClient::new(&env, &token_id.address());

client.initialize(&admin, &token_id.address());
token_client.mint(&user, &1000);

env.ledger().set_timestamp(2_000_000);
client.stake(&user, &100);

env.ledger().set_timestamp(2_000_000 + 604799);
client.unstake(&user);
}

#[test]
fn test_unstake_succeeds_after_lock_and_clears_storage() {
let (env, client) = setup();

let admin = Address::generate(&env);
let user = Address::generate(&env);

let token_id = env.register_stellar_asset_contract_v2(admin.clone());
let token_client = token::StellarAssetClient::new(&env, &token_id.address());

client.initialize(&admin, &token_id.address());
token_client.mint(&user, &1000);

env.ledger().set_timestamp(3_000_000);
client.stake(&user, &250);

env.ledger().set_timestamp(3_000_000 + 604800);
client.unstake(&user);

assert_eq!(token_client.balance(&user), 1000);
assert_eq!(token_client.balance(&client.address), 0);
}

#[test]
#[should_panic(expected = "No stake found")]
fn test_unstake_twice_panics_after_withdrawal() {
let (env, client) = setup();

let admin = Address::generate(&env);
let user = Address::generate(&env);

let token_id = env.register_stellar_asset_contract_v2(admin.clone());
let token_client = token::StellarAssetClient::new(&env, &token_id.address());

client.initialize(&admin, &token_id.address());
token_client.mint(&user, &1000);

env.ledger().set_timestamp(4_000_000);
client.stake(&user, &100);

env.ledger().set_timestamp(4_000_000 + 604800);
client.unstake(&user);

client.unstake(&user);
}
16 changes: 16 additions & 0 deletions contracts/stake-vault/src/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use soroban_sdk::{contracttype, Address};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StakeInfo {
pub amount: i128,
pub lock_timestamp: u64,
}

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