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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
All notable changes to the Sharpy smart contract are documented here.

## [Unreleased]
- feat(contract): `get_fee_bps()` rate view + preview-vs-invoice consistency proof (emitting vs silent share math, release untouched) — feat/fee-consistency (closes #195)
- feat(contract): whitelist enforced uniformly in `pay`/`pay_with_tip`/`pool_pay` + `is_whitelisted_payer(id, payer)` view; `require!` message consistency — feat/whitelist-consistency (closes #194)
- feat(contract): `get_tranche_remaining_bps(id)` view + cumulative-bps invariant (`released + remaining == 10000`) — feat/tranche-remaining (closes #193)
- feat(contract): `resolve_route_chain(id, max_depth)` bounded follower + `get_route_chain_len(id)` cycle-safe depth view — feat/route-chain-depth (closes #192)
Expand Down
8 changes: 8 additions & 0 deletions contracts/sharpy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,15 @@ impl SharpyContract {
env.storage().instance().get(&fee_key())
}

/// Current fee rate in bps (0 when unset). Pure view so dashboards can
/// render the rate without fetching the full `FeeConfig`.
pub fn get_fee_bps(env: Env) -> u32 {
env.storage().instance().get::<Symbol, FeeConfig>(&fee_key()).map(|c| c.fee_bps).unwrap_or(0)
}

/// Preview the protocol fee owed on `amount` (pure query, no state change).
/// Emits `fprev` for indexer visibility; for a silent estimate on an invoice
/// total see `preview_fee_for_invoice` — both share `calc_protocol_fee` math.
pub fn preview_fee(env: Env, amount: i128) -> i128 {
let fee = calc_protocol_fee(&env, amount);
events::fee_previewed(&env, amount, fee);
Expand Down
123 changes: 123 additions & 0 deletions contracts/sharpy/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5760,3 +5760,126 @@ mod test_wl_consistency_c {
assert!(client.is_whitelisted_payer(&id, &payer));
}
}

#[cfg(test)]
mod test_fee_consistency_a {
use soroban_sdk::{testutils::Address as _, Address, Env, Vec};
use crate::SharpyContractClient;
fn setup() -> (Env, SharpyContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
let cid = env.register(crate::SharpyContract, ());
let c = SharpyContractClient::new(&env, &cid);
let a = Address::generate(&env);
let t = Address::generate(&env);
c.initialize(&a, &t);
(env, c)
}
fn mk(env: &Env, client: &SharpyContractClient<'_>, creator: &Address, total: i128) -> u64 {
let r = Address::generate(env);
let tok = Address::generate(env);
let dl = env.ledger().timestamp() + 86400;
let opts = crate::types::InvoiceOptions { escrow_enabled: false, escrow_release_delay: None, split_rules: Vec::new(env), auto_resolve_rules: Vec::new(env), arbitrator: None };
client.create_invoice(creator, &Vec::from_array(env, [r]), &Vec::from_array(env, [total]), &Vec::from_array(env, [tok]), &dl, &opts)
}
#[test]
fn test_fee_bps_view_defaults_zero() {
let (env, client) = setup();
assert_eq!(client.get_fee_bps(), 0u32);
let collector = Address::generate(&env);
client.set_protocol_fee(&250u32, &collector);
assert_eq!(client.get_fee_bps(), 250u32);
}
#[test]
fn test_invoice_preview_matches_amount_preview() {
let (env, client) = setup();
let creator = Address::generate(&env);
let collector = Address::generate(&env);
client.set_protocol_fee(&500u32, &collector);
let id = mk(&env, &client, &creator, 10_000i128);
assert_eq!(client.preview_fee_for_invoice(&id), client.preview_fee(&10_000i128));
}
}

#[cfg(test)]
mod test_fee_consistency_b {
use soroban_sdk::{testutils::Address as _, Address, Env, Vec};
use crate::SharpyContractClient;
fn setup() -> (Env, SharpyContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
let cid = env.register(crate::SharpyContract, ());
let c = SharpyContractClient::new(&env, &cid);
let a = Address::generate(&env);
let t = Address::generate(&env);
c.initialize(&a, &t);
(env, c)
}
fn mk2(env: &Env, client: &SharpyContractClient<'_>, creator: &Address) -> u64 {
let r1 = Address::generate(env);
let r2 = Address::generate(env);
let t1 = Address::generate(env);
let t2 = Address::generate(env);
let dl = env.ledger().timestamp() + 86400;
let opts = crate::types::InvoiceOptions { escrow_enabled: false, escrow_release_delay: None, split_rules: Vec::new(env), auto_resolve_rules: Vec::new(env), arbitrator: None };
client.create_invoice(creator, &Vec::from_array(env, [r1, r2]), &Vec::from_array(env, [6000i128, 4000i128]), &Vec::from_array(env, [t1, t2]), &dl, &opts)
}
#[test]
fn test_multi_recipient_total_preview_matches() {
let (env, client) = setup();
let creator = Address::generate(&env);
let collector = Address::generate(&env);
client.set_protocol_fee(&1000u32, &collector);
let id = mk2(&env, &client, &creator);
assert_eq!(client.preview_fee_for_invoice(&id), client.preview_fee(&10_000i128));
assert_eq!(client.preview_fee_for_invoice(&id), 1000i128);
}
#[test]
fn test_preview_is_pure_double_call() {
let (env, client) = setup();
let creator = Address::generate(&env);
let collector = Address::generate(&env);
client.set_protocol_fee(&250u32, &collector);
let r = Address::generate(&env);
let tok = Address::generate(&env);
let dl = env.ledger().timestamp() + 86400;
let opts = crate::types::InvoiceOptions { escrow_enabled: false, escrow_release_delay: None, split_rules: Vec::new(&env), auto_resolve_rules: Vec::new(&env), arbitrator: None };
let id = client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [8000i128]), &Vec::from_array(&env, [tok]), &dl, &opts);
let a = client.preview_fee_for_invoice(&id);
let b = client.preview_fee_for_invoice(&id);
assert_eq!(a, b);
assert_eq!(a, 200i128);
}
}

#[cfg(test)]
mod test_fee_consistency_c {
use soroban_sdk::{testutils::Address as _, Address, Env};
use crate::SharpyContractClient;
fn setup() -> (Env, SharpyContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
let cid = env.register(crate::SharpyContract, ());
let c = SharpyContractClient::new(&env, &cid);
let a = Address::generate(&env);
let t = Address::generate(&env);
c.initialize(&a, &t);
(env, c)
}
#[test]
fn test_full_bps_equals_amount() {
let (env, client) = setup();
let collector = Address::generate(&env);
client.set_protocol_fee(&10_000u32, &collector);
assert_eq!(client.preview_fee(&12345i128), 12345i128);
assert_eq!(client.get_fee_bps(), 10_000u32);
}
#[test]
fn test_truncation_rounds_down() {
let (env, client) = setup();
let collector = Address::generate(&env);
client.set_protocol_fee(&100u32, &collector);
assert_eq!(client.preview_fee(&1i128), 0i128);
assert_eq!(client.preview_fee(&10_000i128), 100i128);
}
}
Loading