diff --git a/contracts/pool/Cargo.toml b/contracts/pool/Cargo.toml index 18307ad..6ce1ffa 100644 --- a/contracts/pool/Cargo.toml +++ b/contracts/pool/Cargo.toml @@ -16,7 +16,7 @@ testutils = [ ] [dependencies] -soroban-sdk = "20.0.0" +soroban-sdk = "22.0.0" soroban-fixed-point-math = "1.3.0" cast = "0.3.0" sep-40-oracle = "1.2.0" @@ -24,7 +24,7 @@ sep-41-token = "1.2.0" # moderc3156-example = { workspace = true} # Commented to avoid circular dependency [dev-dependencies] -soroban-sdk = { version = "20.0.0", features = ["testutils"] } +soroban-sdk = { version = "22.0.0", features = ["testutils"] } backstop = { path = "../backstop", features = ["testutils"] } sep-40-oracle = { version = "1.2.0", features = ["testutils"] } sep-41-token = { version = "1.2.0", features = ["testutils"] } diff --git a/contracts/pool/src/contract.rs b/contracts/pool/src/contract.rs index 9962094..ed2a887 100644 --- a/contracts/pool/src/contract.rs +++ b/contracts/pool/src/contract.rs @@ -3,7 +3,7 @@ use crate::{ emissions::{self, ReserveEmissionMetadata}, events::PoolEvents, pool::{self, FlashLoan, Positions, Request, Reserve}, - storage::{self, ReserveConfig}, + storage::{self, Investment, ReserveConfig}, PoolConfig, PoolError, ReserveEmissionData, UserEmissionData, }; use soroban_sdk::{ @@ -319,6 +319,28 @@ pub trait Pool { /// * If there is no bad debt to handle /// * If there is an ongoing auction for the user fn bad_debt(e: Env, user: Address); + + /// Internal/utility: Record a lender investment (called from the supply flow). + fn record_investment(e: Env, lender: Address, amount: i128); + + /// Add rewards to a specific lender investment index (index = position in lender vec). + /// This will also update the lender aggregate rewards. + fn add_rewards(e: Env, lender: Address, index: u32, rewards: i128); + + /// Mark an investment withdrawn by index (and reduce total balance) + fn withdraw_investment(e: Env, lender: Address, index: u32); + + /// View: get all investments for a lender (consider pagination later) + fn get_lender_investments(e: Env, lender: Address) -> Vec; + + /// View: total active balance for lender + fn get_lender_total_balance(e: Env, lender: Address) -> i128; + + /// View: total accumulated rewards for lender + fn get_lender_rewards(e: Env, lender: Address) -> i128; + + /// View: investment history for lender (alias for investments) + fn get_lender_investment_history(e: Env, lender: Address) -> Vec; } #[contractimpl] @@ -594,4 +616,101 @@ impl Pool for PoolContract { pool::bad_debt(&e, &user); } + // Investment functions + fn record_investment(e: Env, lender: Address, amount: i128) { + storage::extend_instance(&e); + + // basic checks - adjust as needed + if amount <= 0 { + panic_with_error!(&e, PoolError::BadRequest); + } + + let ts: u64 = e.ledger().timestamp() as u64; + + let inv = Investment { + amount, + timestamp: ts, + returns: 0, + status: 0u32, // active + }; + + // append investment + storage::add_lender_investment(&e, &lender, &inv); + + // increment aggregate balance + storage::inc_lender_total_balance(&e, &lender, amount); + + // emit event + PoolEvents::investment_made(&e, lender.clone(), amount, ts); + } + + fn add_rewards(e: Env, lender: Address, index: u32, rewards: i128) { + storage::extend_instance(&e); + + if rewards <= 0 { + panic_with_error!(&e, PoolError::BadRequest); + } + + // load investments vec, update index + let mut invs = storage::get_lender_investments(&e, &lender); + let len = invs.len(); + if (index as usize) >= len { + panic_with_error!(&e, PoolError::BadRequest); + } + + let mut inv = invs.get(index as usize).unwrap(); + inv.returns = inv.returns + rewards; + // update vec slot + invs.set(index as usize, inv.clone()); + storage::set_lender_investments(&e, &lender, &invs); + + // update aggregate rewards + storage::inc_lender_total_rewards(&e, &lender, rewards); + + // emit event + PoolEvents::rewards_distributed(&e, lender.clone(), rewards); + } + + fn withdraw_investment(e: Env, lender: Address, index: u32) { + storage::extend_instance(&e); + + let mut invs = storage::get_lender_investments(&e, &lender); + let len = invs.len(); + if (index as usize) >= len { + panic_with_error!(&e, PoolError::BadRequest); + } + + let mut inv = invs.get(index as usize).unwrap(); + if inv.status != 0u32 { + // already withdrawn + panic_with_error!(&e, PoolError::BadRequest); + } + + inv.status = 1u32; // withdrawn + invs.set(index as usize, inv.clone()); + storage::set_lender_investments(&e, &lender, &invs); + + // decrement aggregate balance + storage::inc_lender_total_balance(&e, &lender, -inv.amount); + + // emit event + let ts: u64 = e.ledger().timestamp() as u64; + PoolEvents::withdrawal_made(&e, lender.clone(), inv.amount, ts); + } + + fn get_lender_investments(e: Env, lender: Address) -> Vec { + storage::get_lender_investments(&e, &lender) + } + + fn get_lender_total_balance(e: Env, lender: Address) -> i128 { + storage::get_lender_total_balance(&e, &lender) + } + + fn get_lender_rewards(e: Env, lender: Address) -> i128 { + storage::get_lender_total_rewards(&e, &lender) + } + + fn get_lender_investment_history(e: Env, lender: Address) -> Vec { + storage::get_lender_investments(&e, &lender) + } } diff --git a/contracts/pool/src/events.rs b/contracts/pool/src/events.rs index 90fc248..42accf8 100644 --- a/contracts/pool/src/events.rs +++ b/contracts/pool/src/events.rs @@ -1,6 +1,7 @@ use soroban_sdk::{Address, Env, Symbol, Vec}; use crate::{AuctionData, ReserveConfig}; +use soroban_sdk::{Address, Env, Symbol}; pub struct PoolEvents {} @@ -373,4 +374,26 @@ impl PoolEvents { let topics = (Symbol::new(&e, "delete_auction"), auction_type, user); e.events().publish(topics, ()); } + + pub fn investment_made(e: &Env, lender: &Address, amount: i128, timestamp: u64) { + // Topic: ("InvestmentMade", lender) + e.events().publish( + (Symbol::short("InvestmentMade"), lender.clone()), + (amount, timestamp), + ); + } + + pub fn rewards_distributed(e: &Env, lender: &Address, amount: i128) { + e.events().publish( + (Symbol::short("RewardsDistributed"), lender.clone()), + amount, + ); + } + + pub fn withdrawal_made(e: &Env, lender: &Address, amount: i128, timestamp: u64) { + e.events().publish( + (Symbol::short("WithdrawalMade"), lender.clone()), + (amount, timestamp), + ); + } } diff --git a/contracts/pool/src/storage.rs b/contracts/pool/src/storage.rs index 4d51de4..0748103 100644 --- a/contracts/pool/src/storage.rs +++ b/contracts/pool/src/storage.rs @@ -20,6 +20,15 @@ const LEDGER_BUMP_USER: u32 = LEDGER_THRESHOLD_USER + 20 * ONE_DAY_LEDGERS; // ~ /********** Storage Types **********/ +#[derive(Clone, Debug)] +#[contracttype] // Important for Soroban storage serialization +pub struct Investment { + pub amount: i128, // Supplied amount + pub timestamp: u64, // Block timestamp when investment was made + pub returns: i128, // Accumulated rewards/earnings + pub status: u32, // 0 = active, 1 = withdrawn +} + /// The pool's config #[derive(Clone)] #[contracttype] @@ -659,3 +668,75 @@ pub fn del_auction(e: &Env, auction_type: &u32, user: &Address) { }); e.storage().temporary().remove(&key); } + +//investment +// Storage key prefix symbols +const KEY_LENDER_INVESTMENTS: Symbol = Symbol::short("lender_invs"); // (lender -> Vec) +const KEY_LENDER_TOTAL_BAL: Symbol = Symbol::short("lender_bal"); // (lender -> i128) +const KEY_LENDER_TOTAL_REW: Symbol = Symbol::short("lender_rew"); // (lender -> i128) + +/// Return the investments vec for a lender (or an empty Vec if not set). +pub fn get_lender_investments(e: &Env, lender: &Address) -> Vec { + let key = (KEY_LENDER_INVESTMENTS.clone(), lender.clone()); + e.storage() + .instance() + .get(&key) + .unwrap_or_else(|_| Vec::new(e)) +} + +/// Persist the investments vec for a lender. +pub fn set_lender_investments(e: &Env, lender: &Address, invs: &Vec) { + let key = (KEY_LENDER_INVESTMENTS.clone(), lender.clone()); + e.storage().instance().set(&key, invs); +} + +/// Append a new investment to a lender's investment vector and persist. +pub fn add_lender_investment(e: &Env, lender: &Address, inv: &Investment) { + let mut vec = get_lender_investments(e, lender); + vec.push_back(inv.clone()); + set_lender_investments(e, lender, &vec); +} + +/// Get lender total balance (aggregated active amounts). Returns 0 if none. +pub fn get_lender_total_balance(e: &Env, lender: &Address) -> i128 { + let key = (KEY_LENDER_TOTAL_BAL.clone(), lender.clone()); + e.storage() + .instance() + .get(&key) + .unwrap_or(Ok(0i128)) + .unwrap() +} + +/// Set lender total balance. +pub fn set_lender_total_balance(e: &Env, lender: &Address, bal: &i128) { + let key = (KEY_LENDER_TOTAL_BAL.clone(), lender.clone()); + e.storage().instance().set(&key, bal); +} + +/// Increase lender total balance by delta (can be negative to decrease). +pub fn inc_lender_total_balance(e: &Env, lender: &Address, delta: i128) { + let cur = get_lender_total_balance(e, lender); + set_lender_total_balance(e, lender, &(cur + delta)); +} + +/// Get lender total rewards (aggregated). Returns 0 if none. +pub fn get_lender_total_rewards(e: &Env, lender: &Address) -> i128 { + let key = (KEY_LENDER_TOTAL_REW.clone(), lender.clone()); + e.storage() + .instance() + .get(&key) + .unwrap_or(Ok(0i128)) + .unwrap() +} + +/// Set lender total rewards. +pub fn set_lender_total_rewards(e: &Env, lender: &Address, rew: &i128) { + let key = (KEY_LENDER_TOTAL_REW.clone(), lender.clone()); + e.storage().instance().set(&key, rew); +} + +/// Increase lender total rewards by delta. +pub fn inc_lender_total_rewards(e: &Env, lender: &Address, delta: i128) { + let cur = get_lender_total_rewards(e, lender); + set_lender_total_rewards(e, lender, &(cur + delta)); +}