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
61 changes: 49 additions & 12 deletions contracts/revenue_pool/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,52 @@
#![no_std]

//! Revenue settlement contract: receives USDC from vault deducts and distributes to developers.
//!
//! Flow: vault deduct → vault transfers USDC to this contract → admin calls distribute(to, amount).
//!
//! # Security Assumptions
//! - **Admin Key**: The admin has full control over fund distribution. Must be a secure multisig.
//! - **USDC Asset**: The token address is permanently set on initialization. Must be carefully verified.
//! - **Balances / Griefing**: The contract does not rely on strict balance invariants. External transfers
//! increase balance without breaking logic.
//!
//! For detailed threat models and mitigations, see [`SECURITY.md`](../../SECURITY.md).

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

/// Revenue settlement contract: receives USDC from vault deducts and distributes to developers.
///
/// Flow: vault deduct → vault transfers USDC to this contract → admin calls distribute(to, amount).
///
/// # Security Assumptions
/// - **Admin Key**: The admin has full control over fund distribution. Must be a secure multisig.
/// - **USDC Asset**: The token address is permanently set on initialization. Must be carefully verified.
/// - **Balances / Griefing**: The contract does not rely on strict balance invariants. External transfers
/// increase balance without breaking logic.
///
/// For detailed threat models and mitigations, see [`SECURITY.md`](../../SECURITY.md).
/// Storage key for the admin address.
const ADMIN_KEY: &str = "admin";
/// Storage key for the pending admin address during admin transfer.
const PENDING_ADMIN_KEY: &str = "pending_admin";
/// Storage key for the pause guardian address.
const PAUSE_GUARDIAN_KEY: &str = "pause_guardian";
/// Storage key for the USDC token contract address.
const USDC_KEY: &str = "usdc";
/// Storage key for the maximum distribute amount limit.
const MAX_DISTRIBUTE_KEY: &str = "max_distribute";
/// Storage key for tracking cumulative yield deposited.
const CUMULATIVE_YIELD_DEPOSITED_KEY: &str = "cumulative_yield_deposited";
/// Error message: distribution amount must be positive.
const ERR_AMOUNT_NOT_POSITIVE: &str = "amount must be positive";
/// Error message: distribution amount exceeds the configured max_distribute limit.
const ERR_AMOUNT_EXCEEDS_MAX_DISTRIBUTE: &str = "amount exceeds max_distribute";
/// Error message: caller is not the admin.
const ERR_UNAUTHORIZED: &str = "unauthorized: caller is not admin";
/// Error message: caller is not admin or pause guardian.
const ERR_UNAUTHORIZED_PAUSE: &str = "unauthorized: caller is not admin or pause guardian";
/// Error message: contract USDC balance is insufficient for the requested operation.
const ERR_INSUFFICIENT_BALANCE: &str = "insufficient USDC balance";
/// Error message: revenue pool has not been initialized.
const ERR_NOT_INITIALIZED: &str = "revenue pool not initialized";
/// Error message: duplicate recipient address in batch distribute.
const ERR_DUPLICATE_RECIPIENT: &str = "duplicate recipient in batch";
/// Storage key for the paused state flag.
const PAUSED_KEY: &str = "paused";
/// Error message: operation rejected because the contract is paused.
const ERR_PAUSED: &str = "revenue pool paused";
/// Storage key for the current contract version hash.
const VERSION_KEY: &str = "version";

/// Typed contract errors for the revenue pool.
Expand All @@ -48,40 +65,53 @@ pub enum RevenuePoolError {
BatchTooLarge = 2,
}

/// Default maximum distribute amount per call (unlimited).
pub const DEFAULT_MAX_DISTRIBUTE: i128 = i128::MAX;

/// Maximum number of payments allowed in a single `batch_distribute` call.
/// Caps CPU/memory usage well within Soroban resource limits and aligns with
/// the vault's `MAX_BATCH_SIZE` for `batch_deduct`.
pub const MAX_BATCH_SIZE: u32 = 50;
/// Maximum allowed length (in bytes) for admin broadcast messages.
pub const MAX_MESSAGE_LEN: u32 = 256;

/// Severity levels for admin broadcast messages.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Severity {
/// Informational message — no action required.
Info,
/// Warning message — potential issue detected.
Warn,
/// Critical message — immediate attention required.
Crit,
}

/// Event payload for admin broadcast messages.
#[contracttype]
#[derive(Clone, Debug)]
pub struct AdminBroadcast {
/// Severity level of the broadcast.
pub severity: Severity,
/// Broadcast message content.
pub message: String,
}

/// Remaining storage TTL information for a storage category.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct StorageEntryTtl {
/// Storage category name (e.g. "Instance", "Persistent", "Temporary").
pub category: String,
/// Human-readable description of the storage key.
pub key_desc: String,
/// Type of storage (e.g. "Instance", "Persistent").
pub storage_type: String,
/// Current remaining TTL in ledgers.
pub ttl: u32,
/// Threshold below which TTL bumping triggers.
pub threshold: u32,
/// Amount to extend TTL by when bumping.
pub bump_amount: u32,
}

Expand All @@ -94,11 +124,14 @@ pub struct StorageEntryTtl {
/// - `BUMP_AMOUNT`: Number of ledgers to extend TTL by (10000 ledgers ≈ 16 days)
/// - `LIFETIME_THRESHOLD`: Minimum TTL before triggering a bump (1000 ledgers ≈ 1.5 days)
pub const BUMP_AMOUNT: u32 = 10000;
/// Lifetime threshold (in ledgers) below which TTL bumping triggers (~1.5 days).
pub const LIFETIME_THRESHOLD: u32 = 1000;

/// The Revenue Pool contract implementation.
#[contract]
pub struct RevenuePool;

/// Contract implementation block for [`RevenuePool`].
#[contractimpl]
impl RevenuePool {
/// Initialize the revenue pool with an admin and the USDC token address.
Expand Down Expand Up @@ -534,7 +567,11 @@ impl RevenuePool {
}

/// Get the current per-leg distribution cap.
/// Defaults to `i128::MAX` when unset.
///
/// The cap limits the maximum USDC amount that can be distributed in a single
/// [`Self::distribute`] call. Admins raise/lower it via [`Self::set_max_distribute`].
///
/// Defaults to `i128::MAX` when unset (effectively no cap).
pub fn get_max_distribute(env: Env) -> i128 {
env.storage()
.instance()
Expand Down
11 changes: 5 additions & 6 deletions contracts/settlement/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ pub(crate) fn propose_balance_migration(env: &Env, caller: &Address, from: &Addr
let amount: i128 = env
.storage()
.persistent()
.get(&StorageKey::DeveloperBalance(from.clone(), usdc_token.clone()))
.get(&StorageKey::DeveloperBalance(
from.clone(),
usdc_token.clone(),
))
.unwrap_or(0);
if amount <= 0 {
env.panic_with_error(SettlementError::NoDeveloperBalance);
Expand Down Expand Up @@ -76,11 +79,7 @@ pub(crate) fn execute_balance_migration(env: &Env, caller: &Address, from: &Addr
let source_key = StorageKey::DeveloperBalance(from.clone(), usdc_token.clone());
let destination_key = StorageKey::DeveloperBalance(migration.to.clone(), usdc_token.clone());

let source_balance: i128 = env
.storage()
.persistent()
.get(&source_key)
.unwrap_or(0);
let source_balance: i128 = env.storage().persistent().get(&source_key).unwrap_or(0);
let new_source_balance = source_balance
.checked_sub(migration.amount)
.filter(|b| *b >= 0)
Expand Down
4 changes: 3 additions & 1 deletion contracts/settlement/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,7 @@ pub enum SettlementError {
MigrationNotFound = 20,
TimelockNotExpired = 21,
MigrationBalanceChanged = 22,
MinimumBalanceRequired = 23,
OverDraft = 23,
InvalidClaimWindow = 24,
ClaimWindowClosed = 25,
}
5 changes: 4 additions & 1 deletion contracts/settlement/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ mod tests {
#[test]
fn test_event_admin_broadcast_bytes() {
let env = Env::default();
assert_eq!(event_admin_broadcast(&env), Symbol::new(&env, "admin_broadcast"));
assert_eq!(
event_admin_broadcast(&env),
Symbol::new(&env, "admin_broadcast")
);
}

#[test]
Expand Down
Loading
Loading