Skip to content
This repository was archived by the owner on Dec 2, 2025. It is now read-only.
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
4 changes: 2 additions & 2 deletions contracts/pool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ 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"
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"] }
Expand Down
121 changes: 120 additions & 1 deletion contracts/pool/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Investment>;

/// 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<Investment>;
}

#[contractimpl]
Expand Down Expand Up @@ -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);
}
Comment on lines +620 to +699
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Investment mutators need authentication or non-public exposure

record_investment, add_rewards, and withdraw_investment are now in the public Pool trait (Lines 620-699), so anyone can invoke them directly. None of them call require_auth, which lets an attacker (1) credit themselves arbitrary balances via record_investment, (2) mint rewards with add_rewards, or (3) zero out another lender’s position using withdraw_investment. This is a critical authorization hole. Either keep these helpers internal (remove from the trait) or gate them with the appropriate require_auth checks aligned with the supply/reward flows.


fn get_lender_investments(e: Env, lender: Address) -> Vec<Investment> {
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<Investment> {
storage::get_lender_investments(&e, &lender)
}
}
23 changes: 23 additions & 0 deletions contracts/pool/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use soroban_sdk::{Address, Env, Symbol, Vec};

use crate::{AuctionData, ReserveConfig};
use soroban_sdk::{Address, Env, Symbol};

pub struct PoolEvents {}

Expand Down Expand Up @@ -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),
);
}
Comment on lines +378 to +398
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Replace Symbol::short with length-compliant symbols for new events

Symbol::short only accepts ASCII strings up to 9 characters. The literals "InvestmentMade", "RewardsDistributed", and "WithdrawalMade" (Lines 381, 388, 395) all exceed that limit, so these calls will panic every time the events fire. Please switch to Symbol::new(&e, "...") (or shorten the identifiers to ≀9 chars) before shipping.

πŸ€– Prompt for AI Agents
In contracts/pool/src/events.rs around lines 378 to 398, the calls to
Symbol::short use string literals longer than 9 chars which will panic; replace
each Symbol::short("InvestmentMade"), Symbol::short("RewardsDistributed"), and
Symbol::short("WithdrawalMade") with Symbol::new(&e, "<same-name>") (or
alternatively shorten each identifier to ≀9 ASCII chars) so the symbols are
length-compliant; update the three event publish calls accordingly to use
Symbol::new(&e, "...") with the same readable names.

}
81 changes: 81 additions & 0 deletions contracts/pool/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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<Investment>)
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<Investment> {
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<Investment>) {
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));
}
Comment on lines +674 to +742
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Symbol::short constants exceed the permitted 9-character length

Symbol::short("lender_invs"), "lender_bal", and "lender_rew" (Lines 675-677) are all β‰₯10 characters. Symbol::short will panic when the contract loads these keys, breaking every storage call beneath. Please shorten the identifiers to ≀9 characters (e.g., "lend_invs") or create the symbols at runtime with Symbol::new(&e, ...).

πŸ€– Prompt for AI Agents
In contracts/pool/src/storage.rs around lines 674 to 742, the three
Symbol::short calls use identifiers >=10 chars which will panic at contract
load; replace them with either shorter identifiers (<=9 chars) like "lend_invs",
"lend_bal", "lend_rew" or change the constants to be created at runtime using
Symbol::new(&e, "<full_name>") and update all uses accordingly so storage keys
no longer trigger Symbol::short length panics.