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
24 changes: 23 additions & 1 deletion contracts/dividend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
#![no_std]

use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec,
contract, contractimpl, contracttype, token, Address, Env, String, Symbol, Vec,
};

/// ─── Storage TTL ─────────────────────────────────────────────────────────────
///
/// Soroban charges rent on stored entries and evicts them once their TTL
/// elapses unless bumped. Distribution history lives in instance storage, so it
/// is extended at the start of every state-changing call. Stellar closes a
/// ledger roughly every 5 seconds (one day ≈ 17_280 ledgers); instance state is
/// kept alive for ~30 days, with the threshold one day below the target so a
/// bump only pays rent when the entry is within a day of expiry.
const DAY_IN_LEDGERS: u32 = 17_280;
const INSTANCE_BUMP_LEDGERS: u32 = 30 * DAY_IN_LEDGERS;
const INSTANCE_TTL_THRESHOLD: u32 = INSTANCE_BUMP_LEDGERS - DAY_IN_LEDGERS;

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Expand Down Expand Up @@ -39,6 +51,7 @@ impl DividendContract {
env.storage().instance().set(&DataKey::DistributionCounter, &0u32);
env.storage().instance()
.set(&DataKey::Distributions, &Vec::<Distribution>::new(&env));
Self::bump_instance(&env);
}

/// Distribute profit proportionally based on each member's share weight.
Expand All @@ -55,6 +68,7 @@ impl DividendContract {
) -> u32 {
admin.require_auth();
Self::require_admin(&env, &admin);
Self::bump_instance(&env);

if recipients.len() != shares.len() {
panic!("recipients and shares length mismatch");
Expand Down Expand Up @@ -115,6 +129,14 @@ impl DividendContract {
.unwrap_or(Vec::new(&env))
}

/// Extend the contract's instance-storage TTL. Called at the start of every
/// state-changing entrypoint so distribution history is never evicted.
fn bump_instance(env: &Env) {
env.storage()
.instance()
.extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_BUMP_LEDGERS);
}

fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller { panic!("unauthorized"); }
Expand Down
22 changes: 22 additions & 0 deletions contracts/governance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ use soroban_sdk::{
contract, contractimpl, contracttype, Address, Env, Symbol, Vec,
};

/// ─── Storage TTL ─────────────────────────────────────────────────────────────
///
/// Soroban charges rent on stored entries and evicts them once their TTL
/// elapses unless bumped. Group rules live in instance storage, so it is
/// extended at the start of every state-changing call. Stellar closes a ledger
/// roughly every 5 seconds (one day ≈ 17_280 ledgers); instance state is kept
/// alive for ~30 days, with the threshold one day below the target so a bump
/// only pays rent when the entry is within a day of expiry.
const DAY_IN_LEDGERS: u32 = 17_280;
const INSTANCE_BUMP_LEDGERS: u32 = 30 * DAY_IN_LEDGERS;
const INSTANCE_TTL_THRESHOLD: u32 = INSTANCE_BUMP_LEDGERS - DAY_IN_LEDGERS;

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Expand Down Expand Up @@ -55,11 +67,13 @@ impl GovernanceContract {
late_penalty_bps: 200, // 2% penalty
};
env.storage().instance().set(&DataKey::Rules, &rules);
Self::bump_instance(&env);
}

pub fn update_rules(env: Env, admin: Address, rules: CoopRules) {
admin.require_auth();
Self::require_admin(&env, &admin);
Self::bump_instance(&env);
env.storage().instance().set(&DataKey::Rules, &rules);
env.events().publish((Symbol::new(&env, "rules_updated"),), ());
}
Expand All @@ -68,6 +82,14 @@ impl GovernanceContract {
env.storage().instance().get(&DataKey::Rules).unwrap()
}

/// Extend the contract's instance-storage TTL. Called at the start of every
/// state-changing entrypoint so the group's rules are never evicted.
fn bump_instance(env: &Env) {
env.storage()
.instance()
.extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_BUMP_LEDGERS);
}

fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller { panic!("unauthorized"); }
Expand Down
24 changes: 24 additions & 0 deletions contracts/loan/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String,
};

/// ─── Storage TTL ─────────────────────────────────────────────────────────────
///
/// Soroban charges rent on stored entries and evicts them once their TTL
/// elapses unless bumped. This contract keeps loan state in instance storage,
/// so it is extended at the start of every state-changing call. Stellar closes
/// a ledger roughly every 5 seconds (one day ≈ 17_280 ledgers); instance state
/// is kept alive for ~30 days, with the threshold one day below the target so a
/// bump only pays rent when the entry is within a day of expiry.
const DAY_IN_LEDGERS: u32 = 17_280;
const INSTANCE_BUMP_LEDGERS: u32 = 30 * DAY_IN_LEDGERS;
const INSTANCE_TTL_THRESHOLD: u32 = INSTANCE_BUMP_LEDGERS - DAY_IN_LEDGERS;

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Expand Down Expand Up @@ -54,6 +66,7 @@ impl LoanContract {
env.storage().instance().set(&DataKey::AssetAddress, &asset);
env.storage().instance().set(&DataKey::LoanCounter, &0u32);
env.storage().instance().set(&DataKey::Loans, &Vec::<Loan>::new(&env));
Self::bump_instance(&env);
}

/// Member submits a loan request.
Expand All @@ -65,6 +78,7 @@ impl LoanContract {
repayment_days: u32,
) -> u32 {
borrower.require_auth();
Self::bump_instance(&env);
if amount <= 0 { panic!("amount must be positive"); }

let counter: u32 = env.storage().instance()
Expand Down Expand Up @@ -104,6 +118,7 @@ impl LoanContract {
pub fn approve_loan(env: Env, admin: Address, loan_id: u32) {
admin.require_auth();
Self::require_admin(&env, &admin);
Self::bump_instance(&env);

let mut loans: Vec<Loan> = env.storage().instance()
.get(&DataKey::Loans).unwrap();
Expand Down Expand Up @@ -138,6 +153,7 @@ impl LoanContract {
/// Borrower repays (partial or full).
pub fn repay(env: Env, borrower: Address, loan_id: u32, amount: i128) {
borrower.require_auth();
Self::bump_instance(&env);

let mut loans: Vec<Loan> = env.storage().instance()
.get(&DataKey::Loans).unwrap();
Expand Down Expand Up @@ -187,6 +203,14 @@ impl LoanContract {
loans.get(idx).unwrap()
}

/// Extend the contract's instance-storage TTL. Called at the start of every
/// state-changing entrypoint so active loan records are never evicted.
fn bump_instance(env: &Env) {
env.storage()
.instance()
.extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_BUMP_LEDGERS);
}

fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller { panic!("unauthorized"); }
Expand Down
40 changes: 40 additions & 0 deletions contracts/treasury/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String,
};

/// ─── Storage TTL ─────────────────────────────────────────────────────────────
///
/// Soroban charges rent on stored entries and evicts them once their
/// time-to-live (TTL) elapses unless it is explicitly bumped. Instance storage
/// (admin, members, totals) and persistent storage (per-member contribution
/// history) would therefore be silently deleted on an inactive group, so both
/// are extended whenever state changes.
///
/// Stellar closes a ledger roughly every 5 seconds, so one day ≈ 17_280
/// ledgers. Instance config is kept alive for ~30 days; contribution history —
/// long-lived financial data — for ~90 days. The threshold is set one day below
/// the target so a bump only pays rent when the entry is within a day of
/// expiry, rather than on every single call.
const DAY_IN_LEDGERS: u32 = 17_280;
const INSTANCE_BUMP_LEDGERS: u32 = 30 * DAY_IN_LEDGERS;
const INSTANCE_TTL_THRESHOLD: u32 = INSTANCE_BUMP_LEDGERS - DAY_IN_LEDGERS;
const PERSISTENT_BUMP_LEDGERS: u32 = 90 * DAY_IN_LEDGERS;
const PERSISTENT_TTL_THRESHOLD: u32 = PERSISTENT_BUMP_LEDGERS - DAY_IN_LEDGERS;

/// ─── Storage Keys ────────────────────────────────────────────────────────────

#[contracttype]
Expand Down Expand Up @@ -81,6 +100,8 @@ impl TreasuryContract {
env.storage().instance().set(&DataKey::IsActive, &true);
env.storage().instance().set(&DataKey::Members, &Vec::<Address>::new(&env));

Self::bump_instance(&env);

GroupInfo {
name: group_name,
admin,
Expand All @@ -95,6 +116,7 @@ impl TreasuryContract {
pub fn add_member(env: Env, admin: Address, member: Address) {
admin.require_auth();
Self::require_admin(&env, &admin);
Self::bump_instance(&env);

let mut members: Vec<Address> = env
.storage().instance()
Expand All @@ -115,6 +137,7 @@ impl TreasuryContract {
pub fn contribute(env: Env, member: Address, amount: i128, period: u32) {
member.require_auth();
Self::require_member(&env, &member);
Self::bump_instance(&env);

if amount <= 0 {
panic!("amount must be positive");
Expand All @@ -141,6 +164,12 @@ impl TreasuryContract {
history.push_back(record);
env.storage().persistent()
.set(&DataKey::Contributions(member.clone()), &history);
// Keep this member's contribution history alive against TTL eviction.
env.storage().persistent().extend_ttl(
&DataKey::Contributions(member.clone()),
PERSISTENT_TTL_THRESHOLD,
PERSISTENT_BUMP_LEDGERS,
);

// Update total
let total: i128 = env.storage().instance()
Expand All @@ -158,6 +187,7 @@ impl TreasuryContract {
pub fn withdraw(env: Env, admin: Address, to: Address, amount: i128) {
admin.require_auth();
Self::require_admin(&env, &admin);
Self::bump_instance(&env);

let asset: Address = env.storage().instance().get(&DataKey::AssetAddress).unwrap();
let token_client = token::Client::new(&env, &asset);
Expand Down Expand Up @@ -254,6 +284,16 @@ impl TreasuryContract {

// ── Internal helpers ─────────────────────────────────────────────────────

/// Extend the contract's instance-storage TTL. Called at the start of every
/// state-changing entrypoint so an active group never loses its config.
/// Read-only getters intentionally do not bump, keeping them cheap and free
/// of state writes.
fn bump_instance(env: &Env) {
env.storage()
.instance()
.extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_BUMP_LEDGERS);
}

fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller {
Expand Down
42 changes: 42 additions & 0 deletions contracts/voting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ use soroban_sdk::{
contract, contractimpl, contracttype, Address, Env, Map, Symbol, Vec, String,
};

/// ─── Storage TTL ─────────────────────────────────────────────────────────────
///
/// Soroban charges rent on stored entries and evicts them once their
/// time-to-live (TTL) elapses unless it is explicitly bumped. Instance storage
/// (admin, proposal list, counter) and persistent storage (per-proposal vote
/// maps) would otherwise be silently deleted, so both are extended on state
/// changes.
///
/// Stellar closes a ledger roughly every 5 seconds, so one day ≈ 17_280
/// ledgers. Instance config is kept alive for ~30 days; vote maps — the record
/// of who voted — for ~90 days. The threshold is set one day below the target
/// so a bump only pays rent when the entry is within a day of expiry.
const DAY_IN_LEDGERS: u32 = 17_280;
const INSTANCE_BUMP_LEDGERS: u32 = 30 * DAY_IN_LEDGERS;
const INSTANCE_TTL_THRESHOLD: u32 = INSTANCE_BUMP_LEDGERS - DAY_IN_LEDGERS;
const PERSISTENT_BUMP_LEDGERS: u32 = 90 * DAY_IN_LEDGERS;
const PERSISTENT_TTL_THRESHOLD: u32 = PERSISTENT_BUMP_LEDGERS - DAY_IN_LEDGERS;

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Expand Down Expand Up @@ -62,6 +80,7 @@ impl VotingContract {
env.storage().instance().set(&DataKey::TreasuryContract, &treasury);
env.storage().instance().set(&DataKey::ProposalCounter, &0u32);
env.storage().instance().set(&DataKey::Proposals, &Vec::<Proposal>::new(&env));
Self::bump_instance(&env);
}

/// Create a new governance proposal.
Expand All @@ -76,6 +95,7 @@ impl VotingContract {
payload: String,
) -> u32 {
proposer.require_auth();
Self::bump_instance(&env);

let counter: u32 = env.storage().instance()
.get(&DataKey::ProposalCounter).unwrap_or(0);
Expand Down Expand Up @@ -108,6 +128,11 @@ impl VotingContract {
// Initialize empty vote map for this proposal
env.storage().persistent()
.set(&DataKey::Votes(id), &Map::<Address, bool>::new(&env));
env.storage().persistent().extend_ttl(
&DataKey::Votes(id),
PERSISTENT_TTL_THRESHOLD,
PERSISTENT_BUMP_LEDGERS,
);

env.events().publish(
(Symbol::new(&env, "proposal_created"),),
Expand All @@ -119,6 +144,7 @@ impl VotingContract {
/// Member casts a vote on a proposal.
pub fn vote(env: Env, voter: Address, proposal_id: u32, approve: bool) {
voter.require_auth();
Self::bump_instance(&env);

let mut proposals: Vec<Proposal> = env.storage().instance()
.get(&DataKey::Proposals).unwrap();
Expand All @@ -142,6 +168,11 @@ impl VotingContract {

votes.set(voter.clone(), approve);
env.storage().persistent().set(&DataKey::Votes(proposal_id), &votes);
env.storage().persistent().extend_ttl(
&DataKey::Votes(proposal_id),
PERSISTENT_TTL_THRESHOLD,
PERSISTENT_BUMP_LEDGERS,
);

if approve {
proposal.votes_for += 1;
Expand All @@ -160,6 +191,8 @@ impl VotingContract {

/// Finalize a proposal after deadline.
pub fn finalize(env: Env, proposal_id: u32) -> ProposalStatus {
Self::bump_instance(&env);

let mut proposals: Vec<Proposal> = env.storage().instance()
.get(&DataKey::Proposals).unwrap();
let idx = Self::find_proposal_idx(&proposals, proposal_id);
Expand Down Expand Up @@ -204,6 +237,15 @@ impl VotingContract {
.unwrap_or(Map::new(&env))
}

/// Extend the contract's instance-storage TTL. Called at the start of every
/// state-changing entrypoint so an active group never loses its proposals.
/// Read-only getters intentionally do not bump.
fn bump_instance(env: &Env) {
env.storage()
.instance()
.extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_BUMP_LEDGERS);
}

fn find_proposal_idx(proposals: &Vec<Proposal>, id: u32) -> u32 {
for i in 0..proposals.len() {
if proposals.get(i).unwrap().id == id { return i; }
Expand Down
Loading