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
41 changes: 32 additions & 9 deletions contracts/tipz/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub fn require_admin(env: &Env, caller: &Address) -> Result<(), ContractError> {
}

pub fn require_not_paused(env: &Env) -> Result<(), ContractError> {
if storage::is_paused(env) {
if storage::is_paused(env, crate::types::PauseFlag::All) {
return Err(ContractError::ContractPaused);
}
Ok(())
Expand Down Expand Up @@ -144,7 +144,7 @@ pub fn initialize(
storage::set_fee_collector(env, fee_collector);
storage::set_fee_bps(env, fee_bps);
storage::set_native_token(env, native_token);
storage::set_paused(env, false);
storage::set_pause_flags(env, 0); // No pauses by default
storage::set_min_tip_amount(env, 1_000_000_i128);
storage::set_min_withdrawal_amount(env, 1_000_000_i128);
storage::set_version(env, crate::CONTRACT_VERSION);
Expand All @@ -157,13 +157,14 @@ pub fn initialize(
fee_bps,
fee_change_delay_ledgers: DEFAULT_FEE_CHANGE_DELAY_LEDGERS,
native_token: native_token.clone(),
paused: false,
pause_flags: 0,
min_tip_amount: 1_000_000_i128,
rate_limit: crate::types::RateLimitConfig {
max_ops: 50,
window_secs: 3600,
},
domain_reverify_secs: storage::DEFAULT_DOMAIN_REVERIFICATION_INTERVAL,
max_sender_contribution_bps: crate::types::DEFAULT_MAX_SENDER_CONTRIBUTION_BPS,
},
);
storage::set_leaderboard_set(
Expand Down Expand Up @@ -743,12 +744,12 @@ pub fn get_admin_audit_history(
Ok(out)
}

pub fn pause(env: &Env, caller: &Address) -> Result<(), ContractError> {
pub fn pause(env: &Env, caller: &Address, flag: crate::types::PauseFlag) -> Result<(), ContractError> {
storage::extend_instance_ttl(env);
require_admin(env, caller)?;
require_no_multisig(env)?;
storage::set_paused(env, true);
events::emit_contract_paused(env, caller);
storage::set_pause_flag(env, flag, true);
events::emit_contract_paused(env, caller, flag);
log_admin_action(
env,
caller,
Expand All @@ -759,12 +760,12 @@ pub fn pause(env: &Env, caller: &Address) -> Result<(), ContractError> {
Ok(())
}

pub fn unpause(env: &Env, caller: &Address) -> Result<(), ContractError> {
pub fn unpause(env: &Env, caller: &Address, flag: crate::types::PauseFlag) -> Result<(), ContractError> {
storage::extend_instance_ttl(env);
require_admin(env, caller)?;
require_no_multisig(env)?;
storage::set_paused(env, false);
events::emit_contract_unpaused(env, caller);
storage::set_pause_flag(env, flag, false);
events::emit_contract_unpaused(env, caller, flag);
log_admin_action(
env,
caller,
Expand Down Expand Up @@ -805,6 +806,28 @@ pub fn set_min_withdrawal_amount(env: &Env, caller: &Address, amount: i128) -> R
Ok(())
}

/// Set the maximum sender contribution to a creator's leaderboard score in basis points.
/// Admin only. Default is 5000 (50%). Max is 10000 (100%).
pub fn set_max_sender_contribution(
env: &Env,
caller: &Address,
bps: u32,
) -> Result<(), ContractError> {
storage::extend_instance_ttl(env);
require_admin(env, caller)?;
if bps > 10000 {
return Err(ContractError::InvalidInput);
}
let config = storage::get_runtime_config(env).ok_or(ContractError::NotInitialized)?;
let old = config.max_sender_contribution_bps;
if old == bps {
return Ok(());
}
storage::update_runtime_config(env, |c| c.max_sender_contribution_bps = bps);
// Also update the legacy storage if needed
Ok(())
}

/// Admin confirms domain verification after off-chain stellar.toml check.
pub fn verify_domain(env: &Env, caller: &Address, creator: &Address) -> Result<(), ContractError> {
storage::extend_instance_ttl(env);
Expand Down
4 changes: 4 additions & 0 deletions contracts/tipz/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ pub enum ContractError {
InvalidMessage = 48,
SubLimitReached = 49,
RefundReqExpired = 50,
/// Profile is inactive beyond the cleanup threshold
ProfileInactive = 51,
/// Storage limit exceeded for variable-size entry
StorageLimitExceeded = 52,
}

impl ContractError {
Expand Down
63 changes: 59 additions & 4 deletions contracts/tipz/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,16 +343,16 @@ pub fn emit_fee_collected(
),
);
}
pub fn emit_contract_paused(env: &Env, admin: &Address) {
pub fn emit_contract_paused(env: &Env, admin: &Address, flag: crate::types::PauseFlag) {
env.events().publish(
(symbol_short!("contract"), symbol_short!("paused")),
(1u32, admin.clone()),
(1u32, admin.clone(), flag as u32),
);
}
pub fn emit_contract_unpaused(env: &Env, admin: &Address) {
pub fn emit_contract_unpaused(env: &Env, admin: &Address, flag: crate::types::PauseFlag) {
env.events().publish(
(symbol_short!("contract"), symbol_short!("unpaused")),
(1u32, admin.clone()),
(1u32, admin.clone(), flag as u32),
);
}

Expand Down Expand Up @@ -475,6 +475,61 @@ pub fn emit_subscription_executed(
);
}

// ── Scheduled Tip events ─────────────────────────────────────────────────────

/// Topics : `("schedtip", "create")`
pub fn emit_scheduled_tip_created(
env: &Env,
scheduled_tip_id: u32,
sender: &Address,
creator: &Address,
amount: i128,
deliver_at: u64,
) {
env.events().publish(
(
symbol_short!("schedtip"),
symbol_short!("create"),
scheduled_tip_id,
),
(sender.clone(), creator.clone(), amount, deliver_at),
);
}

/// Topics : `("schedtip", "deliver")`
pub fn emit_scheduled_tip_delivered(
env: &Env,
scheduled_tip_id: u32,
creator: &Address,
) {
env.events().publish(
(
symbol_short!("schedtip"),
symbol_short!("deliver"),
scheduled_tip_id,
),
(creator.clone(),),
);
}

/// Topics : `("schedtip", "cancel")`
pub fn emit_scheduled_tip_cancelled(
env: &Env,
scheduled_tip_id: u32,
sender: &Address,
refund_amount: i128,
cancellation_fee: i128,
) {
env.events().publish(
(
symbol_short!("schedtip"),
symbol_short!("cancel"),
scheduled_tip_id,
),
(sender.clone(), refund_amount, cancellation_fee),
);
}

// ── Withdrawal Scheduling events ─────────────────────────────────────────────

/// Topics : `("wd", "sched")`
Expand Down
18 changes: 16 additions & 2 deletions contracts/tipz/src/leaderboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ fn find_insertion_index(entries: &Vec<LeaderboardEntry>, amount: i128) -> u32 {
/// 4. **Evict** the now-lowest entry if the insert pushed the list over the
/// cap, keeping exactly the top `MAX_LEADERBOARD_SIZE`.
fn update_entries(entries: &mut Vec<LeaderboardEntry>, profile: &Profile, amount: i128) {
// Step 0 — trim any pre-existing oversized list to the cap.
while entries.len() > MAX_LEADERBOARD_SIZE {
entries.pop_back();
}

// Step 1 — drop the creator's stale entry if they are already ranked.
let mut i: u32 = 0;
while i < entries.len() {
Expand Down Expand Up @@ -396,10 +401,19 @@ mod tests {
assert_eq!(result.len(), 50);
assert_eq!(result.get(0).unwrap().address, addr_new);

// Lowest (10) should be gone
// Highest old score (500) should be evicted; lowest old (10) should remain
let mut min_amount = i128::MAX;
let mut found_500 = false;
for e in result.iter() {
assert!(e.amount > 10 || e.address == addr_new);
if e.amount < min_amount {
min_amount = e.amount;
}
if e.amount == 500 {
found_500 = true;
}
}
assert_eq!(min_amount, 10);
assert!(!found_500);
});
}
}
61 changes: 50 additions & 11 deletions contracts/tipz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

#![no_std]

#[cfg(any(test, feature = "testutils"))]
extern crate std;

pub mod admin;
pub mod credit;
pub mod errors;
Expand All @@ -21,8 +24,8 @@ pub mod fees;
pub mod goals;
pub mod leaderboard;
pub mod migrations;
pub mod multisig;
pub mod multitoken;
pub mod multisig;
pub mod oracle;
pub mod profile;
pub mod refund;
Expand Down Expand Up @@ -110,6 +113,15 @@ impl TipzContract {
profile::update_profile(&env, caller, display_name, bio, image_url, x_handle)
}

/// Update social links for a profile with limit enforcement (max 5 links).
pub fn update_social_links(
env: Env,
caller: Address,
social_links: soroban_sdk::Map<soroban_sdk::Symbol, String>,
) -> Result<(), ContractError> {
profile::update_social_links(&env, caller, social_links)
}

/// Deregister the caller's profile, permanently removing it from the platform.
///
/// # Requirements
Expand Down Expand Up @@ -302,16 +314,22 @@ impl TipzContract {
storage::get_migration_state(&env)
}

/// Get a single tip record by its ID.
/// Get a single tip record by its ID (public view).
///
/// For anonymous tips the sender is redacted to the contract address;
/// the stable `pseudonym` hash is still returned so clients can group
/// tips from one anonymous tipper without learning who sent them.
///
/// Returns [`ContractError::NotFound`] when the tip does not exist or its
/// temporary-storage TTL has expired (~7 days after the tip was sent).
pub fn get_tip(env: Env, tip_id: u32) -> Result<Tip, ContractError> {
tips::get_tip(&env, tip_id).ok_or(ContractError::NotFound)
tips::get_tip_public(&env, tip_id).ok_or(ContractError::NotFound)
}

/// Return up to `limit` recent tips received by `creator`, newest first.
///
/// - Anonymous tips have their sender redacted to the contract address;
/// use the stable `pseudonym` hash to group them instead.
/// - `limit` is capped at 50 per call.
/// - `offset`: number of tips to skip from the most recent (0 = start
/// from latest). Use `get_creator_tip_count` to know the total for
Expand Down Expand Up @@ -764,16 +782,16 @@ impl TipzContract {
admin::upgrade(&env, &admin, &new_wasm_hash)
}

pub fn pause(env: Env, caller: Address) -> Result<(), ContractError> {
admin::pause(&env, &caller)
pub fn pause(env: Env, caller: Address, flag: u32) -> Result<(), ContractError> {
admin::pause(&env, &caller, crate::types::PauseFlag::from_u32(flag))
}

pub fn unpause(env: Env, caller: Address) -> Result<(), ContractError> {
admin::unpause(&env, &caller)
pub fn unpause(env: Env, caller: Address, flag: u32) -> Result<(), ContractError> {
admin::unpause(&env, &caller, crate::types::PauseFlag::from_u32(flag))
}

pub fn is_paused(env: Env) -> bool {
storage::is_paused(&env)
pub fn is_paused(env: Env, flag: u32) -> bool {
storage::is_paused(&env, crate::types::PauseFlag::from_u32(flag))
}

pub fn set_min_tip_amount(
Expand All @@ -788,8 +806,29 @@ impl TipzContract {
storage::get_min_tip_amount(&env)
}

pub fn set_min_withdrawal_amount(env: Env, caller: Address, amount: i128) -> Result<(), ContractError> { admin::set_min_withdrawal_amount(&env, &caller, amount) }
pub fn get_min_withdrawal_amount(env: Env) -> i128 { storage::get_min_withdrawal_amount(&env) }
/// Set the minimum withdrawal amount. Admin only.
pub fn set_min_withdrawal_amount(
env: Env,
caller: Address,
amount: i128,
) -> Result<(), ContractError> {
admin::set_min_withdrawal_amount(&env, &caller, amount)
}

/// Get the minimum withdrawal amount.
pub fn get_min_withdrawal_amount(env: Env) -> i128 {
storage::get_min_withdrawal_amount(&env)
}

/// Set the maximum sender contribution to a creator's leaderboard score in basis points.
/// Admin only.
pub fn set_max_sender_contribution(
env: Env,
caller: Address,
bps: u32,
) -> Result<(), ContractError> {
admin::set_max_sender_contribution(&env, &caller, bps)
}

/// Update rate limit configuration. Admin only.
pub fn set_rate_limit_config(
Expand Down
8 changes: 4 additions & 4 deletions contracts/tipz/src/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,12 @@ fn execute_proposal_internal(
// Execute the action
match proposal.action {
Action::Pause => {
storage::set_paused(env, true);
crate::events::emit_contract_paused(env, &env.current_contract_address());
storage::set_pause_flag(env, crate::types::PauseFlag::All, true);
crate::events::emit_contract_paused(env, &env.current_contract_address(), crate::types::PauseFlag::All);
}
Action::Unpause => {
storage::set_paused(env, false);
crate::events::emit_contract_unpaused(env, &env.current_contract_address());
storage::set_pause_flag(env, crate::types::PauseFlag::All, false);
crate::events::emit_contract_unpaused(env, &env.current_contract_address(), crate::types::PauseFlag::All);
}
Action::Upgrade(wasm_hash) => {
env.deployer().update_current_contract_wasm(wasm_hash);
Expand Down
2 changes: 1 addition & 1 deletion contracts/tipz/src/multitoken.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ pub fn send_tip_token(
) -> Result<(), ContractError> {
storage::extend_instance_ttl(env);
let config = storage::get_runtime_config(env).ok_or(ContractError::NotInitialized)?;
if config.paused {
if storage::is_paused(env, crate::types::PauseFlag::Tips) || storage::is_paused(env, crate::types::PauseFlag::All) {
return Err(ContractError::ContractPaused);
}
tipper.require_auth();
Expand Down
Loading
Loading