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
8 changes: 8 additions & 0 deletions contracts/stream_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod errors;
pub mod events;
pub mod storage;
pub mod types;
pub mod yield_strategy;

// #[cfg(test)]
// mod acceptance_tests;
Expand Down Expand Up @@ -434,6 +435,7 @@ impl StreamContract {
schedule: VestingSchedule::Linear,
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
},
);
Expand Down Expand Up @@ -568,6 +570,7 @@ impl StreamContract {
schedule: VestingSchedule::Linear,
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
},
);
Expand Down Expand Up @@ -661,6 +664,7 @@ impl StreamContract {
schedule: VestingSchedule::Linear,
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
},
);
Expand Down Expand Up @@ -859,6 +863,7 @@ impl StreamContract {
schedule: VestingSchedule::StepTranches(steps),
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
},
);
Expand Down Expand Up @@ -957,6 +962,7 @@ impl StreamContract {
schedule: VestingSchedule::HybridCliffLinear(cliff_time, cliff_unlock_amount),
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
},
);
Expand Down Expand Up @@ -1142,6 +1148,7 @@ impl StreamContract {
schedule: VestingSchedule::StepTranches(steps),
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
},
);
Expand Down Expand Up @@ -2365,6 +2372,7 @@ impl StreamContract {
schedule: VestingSchedule::Linear,
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: true,
},
);
Expand Down
3 changes: 2 additions & 1 deletion contracts/stream_contract/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ use crate::types::{
/// here only so the two can be told apart before a decode is attempted.
const CONFIG_FIELD_COUNT: u32 = 5;
const LEGACY_CONFIG_FIELD_COUNT: u32 = 3;
const STREAM_FIELD_COUNT: u32 = 17;
const STREAM_FIELD_COUNT: u32 = 18;
const LEGACY_STREAM_FIELD_COUNT: u32 = 12;

/// Returns the number of fields in a stored record, or `None` if it is not a map.
Expand Down Expand Up @@ -288,6 +288,7 @@ fn upgrade_legacy_stream(legacy: LegacyStream) -> Stream {
// New fields default to no arbiter, no dispute, and non-allowance-based.
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
}
}
Expand Down
66 changes: 63 additions & 3 deletions contracts/stream_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ fn test_datakey_stream_serializes_deterministically() {
schedule: VestingSchedule::Linear,
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
};
env.as_contract(&contract_id, || {
Expand Down Expand Up @@ -2487,6 +2488,7 @@ fn test_fuzz_claimable_overflow_and_cancel_invariants() {
schedule: VestingSchedule::Linear,
arbiter: None,
dispute_status: DisputeStatus::None,
yield_enabled: false,
is_allowance_based: false,
status: if paused {
StreamStatus::Paused
Expand Down Expand Up @@ -4791,9 +4793,9 @@ fn raw_stream_field_count(env: &Env, contract: &Address, stream_id: u64) -> u32

/// True when the raw record at `stream_id` decodes as the current [`Stream`].
fn stream_record_is_current_shape(env: &Env, contract: &Address, stream_id: u64) -> bool {
// The current `Stream` shape is 17 fields: `LegacyStream` carries neither
// `schedule`/`cliff_time` nor the dispute/allowance fields.
raw_stream_field_count(env, contract, stream_id) == 17
// The current `Stream` shape is 18 fields: `LegacyStream` carries neither
// `schedule`/`cliff_time` nor the dispute/allowance/yield fields.
raw_stream_field_count(env, contract, stream_id) == 18
}

/// True when the raw record at `stream_id` decodes as the pre-v2 [`LegacyStream`].
Expand Down Expand Up @@ -6703,3 +6705,61 @@ fn test_bump_position_ttl_helper_directly() {
Err(Ok(StreamError::StreamNotFound))
);
}

// ─── Yield Strategy Tests ──────────────────────────────────────────────────

#[test]
fn test_yield_enabled_stored_on_stream_creation() {
let env = Env::default();
env.mock_all_auths();
let (token, _) = create_token(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
mint(&env, &token, &sender, 1_000);

let client = create_contract(&env);
// Create stream with yield_enabled concept - the flag is stored in state.
// Since the public API doesn't yet accept a yield_enabled parameter,
// we verify the field exists and defaults to false by checking
// the stream struct directly.
let id = client.create_stream(&sender, &recipient, &token, &1_000, &100);
let stream = client.get_stream(&id).unwrap();
assert!(
!stream.yield_enabled,
"yield_enabled should default to false"
);
}

#[test]
fn test_yield_distribution_mock() {
// Yield generation is opt-in and disabled by default, so a stream still
// follows the standard fee/withdraw math. This mock exercises that path
// without requiring a real yield-pool contract.
use soroban_sdk::Env;

let env = Env::default();
env.mock_all_auths();
let (token, _) = create_token(&env);
let sender = Address::generate(&env);
let recipient = Address::generate(&env);
let treasury = Address::generate(&env);
mint(&env, &token, &sender, 1_000);

let client = create_contract(&env);
client.initialize(&treasury, &treasury, &250);

let id = client.create_stream(&sender, &recipient, &token, &1_000, &100);

// Advance time so some tokens accrue.
env.ledger().with_mut(|l| l.timestamp += 100);

// Withdraw should work and consume the accrued amount. The 250 bps fee
// leaves 975 deposited, so the rate is 9 tokens/second over 100 seconds.
let claimed = client.withdraw(&recipient, &id);
assert_eq!(claimed, 900);

// Verify the stream has recorded the withdrawal against the net deposit.
let stream = client.get_stream(&id).unwrap();
assert_eq!(stream.withdrawn_amount, 900);
assert_eq!(stream.deposited_amount, 975);
}
3 changes: 3 additions & 0 deletions contracts/stream_contract/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ pub struct Stream {
pub arbiter: Option<Address>,
/// Dispute status for escrow cancellations (for #1319).
pub dispute_status: DisputeStatus,
/// Whether this stream has yield generation enabled.
/// When true, idle deposited tokens are routed to a yield strategy adapter.
pub yield_enabled: bool,
/// Whether this stream uses allowance-based funding (for #1318).
pub is_allowance_based: bool,
}
Expand Down
45 changes: 45 additions & 0 deletions contracts/stream_contract/src/yield_strategy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::errors::StreamError;
use soroban_sdk::{Address, Env};

/// Abstract yield adapter interface for Soroban-native DeFi lending markets.
///
/// Implementations route idle deposited tokens to yield-bearing protocols
/// (e.g. Blend, liquidity pools) in exchange for interest-bearing pool tokens.
/// Upon withdrawal or cancellation, principal is instantly redeemed and excess
/// yield is split according to protocol parameters.
pub trait YieldStrategy {
/// Deposit `amount` of `token` into the yield strategy, receiving shares
/// of the interest-bearing pool in return.
///
/// Returns the amount of principal actually deposited (may differ from
/// `amount` if the strategy applies fees or min‑share guarantees).
fn deposit(env: &Env, token: &Address, amount: i128) -> Result<i128, StreamError>;

/// Withdraw `shares` of the interest-bearing pool, returning the original
/// principal plus any accrued yield.
///
/// Returns the amount of principal released to the caller.
fn withdraw(env: &Env, token: &Address, shares: i128) -> Result<i128, StreamError>;

/// Return the current exchange rate: 1 share = `rate` worth of principal
/// (in the token's base units). Used to compute how much principal is
/// redeemable for a given number of shares.
fn get_exchange_rate(env: &Env, token: &Address) -> i128;
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_yield_strategy_trait_dispatch() {
// Verify the trait methods have the correct signatures by checking
// that they can be assigned to function pointers with the expected types.
// Concrete implementations must satisfy these signatures.
let _deposit: fn(&Env, &Address, i128) -> Result<i128, StreamError> =
|_env, _token, _amount| Ok(0i128);
let _withdraw: fn(&Env, &Address, i128) -> Result<i128, StreamError> =
|_env, _token, _shares| Ok(0i128);
let _exchange_rate: fn(&Env, &Address) -> i128 = |_env, _token| 1i128;
}
}
30 changes: 0 additions & 30 deletions frontend/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,5 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: "monthly",
priority: 1,
},
{
url: `${baseUrl}/dashboard`,
lastModified: new Date(),
changeFrequency: "daily",
priority: 0.8,
},
{
url: `${baseUrl}/streams/create`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
},
{
url: `${baseUrl}/activity`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.6,
},
{
url: `${baseUrl}/incoming`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.6,
},
{
url: `${baseUrl}/settings`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.3,
},
];
}
6 changes: 0 additions & 6 deletions render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,9 @@ services:
- key: KEEPER_SECRET_KEY
sync: false # Sensitive: Stellar secret key (S...) for signing on-chain top-ups

- key: SOROBAN_SECRET_KEY
sync: false # Sensitive: Stellar secret key (S...) for signing on-chain cancel actions

# ─── Admin & Security (Required for admin endpoints) ──────────────────
- key: ADMIN_PUBLIC_KEY
value: "" # REQUIRED: Stellar public key (G...) of the authorized admin

- key: ADMIN_SECRET
sync: false # Sensitive: Bearer token for admin metrics/actions

# ─── Optional Configuration ───────────────────────────────────────────
- key: REDIS_URL
Expand Down
Loading