Skip to content

Commit 91010e3

Browse files
authored
Merge pull request #250 from Ajadu-Saviour/feat/creator-self-spending-limit
Feat/creator self spending limit
2 parents 706ae07 + b8d8b1f commit 91010e3

3 files changed

Lines changed: 364 additions & 0 deletions

File tree

contracts/split/src/lib.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,27 @@ fn creator_volume_used_key(creator: &Address) -> (Symbol, Address) {
362362
(symbol_short!("cr_v_use"), creator.clone())
363363
}
364364

365+
/// Issue #241: Per-creator self-imposed daily spending limit.
366+
fn creator_self_limit_key(creator: &Address) -> (Symbol, Address) {
367+
(symbol_short!("cr_slf_lim"), creator.clone())
368+
}
369+
370+
/// Issue #241: Per-creator self-imposed daily spending used (for the current day).
371+
fn creator_self_used_key(creator: &Address) -> (Symbol, Address) {
372+
(symbol_short!("cr_slf_use"), creator.clone())
373+
}
374+
375+
/// Issue #241: Per-creator last day timestamp when self-limit was checked/reset.
376+
fn creator_self_limit_day_key(creator: &Address) -> (Symbol, Address) {
377+
(symbol_short!("cr_slf_day"), creator.clone())
378+
}
379+
380+
/// Issue #241: Per-creator pending raise request for self-limit.
381+
/// Stores the new limit amount that's waiting to be executed after timelock.
382+
fn creator_self_limit_raise_key(creator: &Address) -> (Symbol, Address) {
383+
(symbol_short!("cr_slf_rse"), creator.clone())
384+
}
385+
365386
fn fee_tiers_key() -> Symbol {
366387
symbol_short!("fee_trs")
367388
}
@@ -992,6 +1013,133 @@ impl SplitContract {
9921013
.unwrap_or(0)
9931014
}
9941015

1016+
// -----------------------------------------------------------------------
1017+
// Issue #241: Creator self-imposed spending limit
1018+
// -----------------------------------------------------------------------
1019+
1020+
/// Set or lower a self-imposed daily spending limit for the caller (creator).
1021+
/// Requires the creator's own auth. Can only lower the limit immediately.
1022+
/// To raise the limit, use request_raise_self_limit() + timelock.
1023+
///
1024+
/// A limit of 0 means no self-imposed limit (unrestricted).
1025+
///
1026+
/// # Arguments
1027+
/// * `env` - The Soroban environment
1028+
/// * `creator` - The creator address (must authenticate)
1029+
/// * `new_limit` - The new daily spending limit (must be >= 0)
1030+
pub fn set_self_limit(env: Env, creator: Address, new_limit: i128) {
1031+
creator.require_auth();
1032+
assert!(new_limit >= 0, "self limit must be non-negative");
1033+
1034+
let current_limit: i128 = env
1035+
.storage()
1036+
.persistent()
1037+
.get(&creator_self_limit_key(&creator))
1038+
.unwrap_or(0);
1039+
1040+
// Allow immediate lowering or setting from 0
1041+
if current_limit > 0 {
1042+
assert!(
1043+
new_limit <= current_limit,
1044+
"cannot raise limit directly; use request_raise_self_limit()"
1045+
);
1046+
}
1047+
1048+
env.storage()
1049+
.persistent()
1050+
.set(&creator_self_limit_key(&creator), &new_limit);
1051+
1052+
// Reset the daily usage counter when changing the limit
1053+
env.storage()
1054+
.persistent()
1055+
.set(&creator_self_used_key(&creator), &0i128);
1056+
1057+
append_audit_entry(&env, 0, symbol_short!("slf_lim"), &creator);
1058+
}
1059+
1060+
/// Request to raise the creator's self-imposed daily spending limit.
1061+
/// This queues a timelocked action. Once the timelock expires,
1062+
/// execute_action() must be called to apply the new limit.
1063+
///
1064+
/// Requires the creator's own auth.
1065+
///
1066+
/// # Arguments
1067+
/// * `env` - The Soroban environment
1068+
/// * `creator` - The creator address (must authenticate)
1069+
/// * `new_limit` - The desired new limit (must be > current limit)
1070+
///
1071+
/// # Returns
1072+
/// The action_id of the queued raise request.
1073+
pub fn request_raise_self_limit(env: Env, creator: Address, new_limit: i128) -> u64 {
1074+
creator.require_auth();
1075+
assert!(new_limit >= 0, "new limit must be non-negative");
1076+
1077+
let current_limit: i128 = env
1078+
.storage()
1079+
.persistent()
1080+
.get(&creator_self_limit_key(&creator))
1081+
.unwrap_or(0);
1082+
1083+
assert!(
1084+
new_limit > current_limit,
1085+
"new limit must be higher than current limit"
1086+
);
1087+
1088+
// Queue the timelock action using the existing timelock mechanism
1089+
let action = TimelockAction::RaiseCreatorSelfLimit(creator.clone(), new_limit);
1090+
1091+
// Get and increment the action counter
1092+
let mut counter: u64 = env
1093+
.storage()
1094+
.persistent()
1095+
.get(&timelock_action_counter_key())
1096+
.unwrap_or(0u64);
1097+
counter = counter.checked_add(1).expect("action counter overflow");
1098+
1099+
let now = env.ledger().timestamp();
1100+
let queued = QueuedAction {
1101+
action,
1102+
queued_at: now,
1103+
executed: false,
1104+
};
1105+
1106+
env.storage().persistent().set(&timelock_action_key(counter), &queued);
1107+
env.storage().persistent().set(&timelock_action_counter_key(), &counter);
1108+
1109+
append_audit_entry(&env, 0, symbol_short!("req_rse"), &creator);
1110+
1111+
counter
1112+
}
1113+
1114+
/// Get the current self-imposed daily spending limit for a creator (0 = no limit).
1115+
pub fn get_self_limit(env: Env, creator: Address) -> i128 {
1116+
env.storage()
1117+
.persistent()
1118+
.get(&creator_self_limit_key(&creator))
1119+
.unwrap_or(0)
1120+
}
1121+
1122+
/// Get the amount of the daily spending limit used so far for a creator.
1123+
pub fn get_self_limit_used(env: Env, creator: Address) -> i128 {
1124+
let today_start = (env.ledger().timestamp() / 86_400) * 86_400;
1125+
1126+
let last_day: u64 = env
1127+
.storage()
1128+
.persistent()
1129+
.get(&creator_self_limit_day_key(&creator))
1130+
.unwrap_or(0);
1131+
1132+
// If we're in a new day, reset the counter
1133+
if last_day != today_start {
1134+
return 0;
1135+
}
1136+
1137+
env.storage()
1138+
.persistent()
1139+
.get(&creator_self_used_key(&creator))
1140+
.unwrap_or(0)
1141+
}
1142+
9951143
// -----------------------------------------------------------------------
9961144
// Issue #188: Dispute arbitration
9971145
// -----------------------------------------------------------------------
@@ -1468,6 +1616,17 @@ impl SplitContract {
14681616
assert!(*new_fee <= 10_000, "platform_fee_bps must be ≤ 10000");
14691617
env.storage().instance().set(&platform_fee_bps_key(), new_fee);
14701618
}
1619+
TimelockAction::RaiseCreatorSelfLimit(creator, new_limit) => {
1620+
// Issue #241: Apply the raise to the self-limit
1621+
env.storage()
1622+
.persistent()
1623+
.set(&creator_self_limit_key(creator), new_limit);
1624+
1625+
// Reset daily usage when limit is raised
1626+
env.storage()
1627+
.persistent()
1628+
.set(&creator_self_used_key(creator), &0i128);
1629+
}
14711630
}
14721631

14731632
queued.executed = true;

contracts/split/src/test.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5098,3 +5098,206 @@ fn test_migrate_escrow_zero_balance() {
50985098
let tk = token_client(&env, &token_id);
50995099
assert_eq!(tk.balance(&new_contract), 0);
51005100
}
5101+
5102+
5103+
// ---------------------------------------------------------------------------
5104+
// Creator Self-Imposed Spending Limit Tests (Issue #241)
5105+
// ---------------------------------------------------------------------------
5106+
5107+
#[test]
5108+
fn test_creator_self_limit_immediate_lower() {
5109+
let (env, contract_id, token_id) = setup();
5110+
let c = client(&env, &contract_id);
5111+
5112+
let creator = Address::generate(&env);
5113+
let recipient = Address::generate(&env);
5114+
5115+
env.ledger().set_timestamp(1_000);
5116+
5117+
// Creator sets self limit to 500
5118+
c.set_self_limit(&creator, &500_i128);
5119+
assert_eq!(c.get_self_limit(&creator), 500);
5120+
5121+
// Creator immediately lowers it to 200
5122+
c.set_self_limit(&creator, &200_i128);
5123+
assert_eq!(c.get_self_limit(&creator), 200);
5124+
}
5125+
5126+
#[test]
5127+
#[should_panic(expected = "cannot raise limit directly")]
5128+
fn test_creator_self_limit_immediate_raise_blocked() {
5129+
let (env, contract_id, token_id) = setup();
5130+
let c = client(&env, &contract_id);
5131+
5132+
let creator = Address::generate(&env);
5133+
5134+
env.ledger().set_timestamp(1_000);
5135+
5136+
// Creator sets self limit to 200
5137+
c.set_self_limit(&creator, &200_i128);
5138+
assert_eq!(c.get_self_limit(&creator), 200);
5139+
5140+
// Creator attempts to immediately raise it (should panic)
5141+
c.set_self_limit(&creator, &500_i128);
5142+
}
5143+
5144+
#[test]
5145+
fn test_creator_self_limit_raise_via_timelock() {
5146+
let (env, contract_id, token_id) = setup();
5147+
let c = client(&env, &contract_id);
5148+
let admin = Address::generate(&env);
5149+
let treasury = Address::generate(&env);
5150+
let creator = Address::generate(&env);
5151+
5152+
// Initialize with a timelock delay of 7 days
5153+
let timelock_secs = 7 * 24 * 60 * 60u64; // 7 days in seconds
5154+
c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &0_u32, &0_u32, &0_u64);
5155+
c.set_timelock_secs(&admin, timelock_secs);
5156+
5157+
env.ledger().set_timestamp(1_000);
5158+
5159+
// Creator sets self limit to 200
5160+
c.set_self_limit(&creator, &200_i128);
5161+
assert_eq!(c.get_self_limit(&creator), 200);
5162+
5163+
// Creator requests to raise limit to 500
5164+
let action_id = c.request_raise_self_limit(&creator, &500_i128);
5165+
5166+
// Attempt to use the raised limit immediately (should still be 200)
5167+
assert_eq!(c.get_self_limit(&creator), 200);
5168+
5169+
// Advance time past timelock
5170+
env.ledger().set_timestamp(1_000 + timelock_secs + 1);
5171+
5172+
// Execute the action
5173+
c.execute_action(&action_id);
5174+
5175+
// Now the limit should be raised to 500
5176+
assert_eq!(c.get_self_limit(&creator), 500);
5177+
}
5178+
5179+
#[test]
5180+
fn test_creator_self_limit_enforced_in_create_invoice() {
5181+
let (env, contract_id, token_id) = setup();
5182+
let c = client(&env, &contract_id);
5183+
5184+
let creator = Address::generate(&env);
5185+
let payer = Address::generate(&env);
5186+
let recipient = Address::generate(&env);
5187+
5188+
StellarAssetClient::new(&env, &token_id).mint(&payer, &1_000_i128);
5189+
env.ledger().set_timestamp(1_000);
5190+
5191+
// Creator sets self limit to 300
5192+
c.set_self_limit(&creator, &300_i128);
5193+
5194+
// Create invoice for 200 - should succeed
5195+
let id1 = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999);
5196+
assert_eq!(c.get_invoice(&id1).status, InvoiceStatus::Pending);
5197+
5198+
// Try to create another invoice for 200 - should fail (total would be 400 > 300)
5199+
let err_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5200+
make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999)
5201+
}));
5202+
assert!(err_result.is_err(), "Expected panic when self limit exceeded");
5203+
}
5204+
5205+
#[test]
5206+
fn test_creator_self_limit_daily_reset() {
5207+
let (env, contract_id, token_id) = setup();
5208+
let c = client(&env, &contract_id);
5209+
5210+
let creator = Address::generate(&env);
5211+
let recipient = Address::generate(&env);
5212+
5213+
StellarAssetClient::new(&env, &token_id).mint(&creator, &1_000_i128);
5214+
env.ledger().set_timestamp(1_000);
5215+
5216+
// Creator sets self limit to 500
5217+
c.set_self_limit(&creator, &500_i128);
5218+
5219+
// Create invoice for 300
5220+
let _id1 = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999);
5221+
assert_eq!(c.get_self_limit_used(&creator), 300);
5222+
5223+
// Try to create another invoice for 300 on the same day (would exceed 500)
5224+
let err_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5225+
make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999)
5226+
}));
5227+
assert!(err_result.is_err(), "Expected panic when exceeding daily self limit");
5228+
5229+
// Advance to next day (add 86400 seconds)
5230+
env.ledger().set_timestamp(1_000 + 86_400 + 1);
5231+
5232+
// Now creating invoice for 300 should succeed (daily reset)
5233+
let _id2 = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999);
5234+
assert_eq!(c.get_self_limit_used(&creator), 300); // Reset for new day
5235+
}
5236+
5237+
#[test]
5238+
fn test_creator_self_limit_zero_means_unlimited() {
5239+
let (env, contract_id, token_id) = setup();
5240+
let c = client(&env, &contract_id);
5241+
5242+
let creator = Address::generate(&env);
5243+
let recipient = Address::generate(&env);
5244+
5245+
StellarAssetClient::new(&env, &token_id).mint(&creator, &10_000_i128);
5246+
env.ledger().set_timestamp(1_000);
5247+
5248+
// Creator sets self limit to 0 (no limit)
5249+
c.set_self_limit(&creator, &0_i128);
5250+
5251+
// Create multiple large invoices - should all succeed
5252+
let _id1 = make_invoice(&env, &c, &creator, &recipient, 5_000, &token_id, 9_999);
5253+
let _id2 = make_invoice(&env, &c, &creator, &recipient, 3_000, &token_id, 9_999);
5254+
let _id3 = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999);
5255+
5256+
// No panic should occur
5257+
}
5258+
5259+
#[test]
5260+
#[should_panic(expected = "only creator can")]
5261+
fn test_creator_self_limit_requires_creator_auth() {
5262+
let (env, contract_id, token_id) = setup();
5263+
let c = client(&env, &contract_id);
5264+
5265+
let creator = Address::generate(&env);
5266+
let other_addr = Address::generate(&env);
5267+
5268+
// Try to set limit for creator with different address's auth (should panic)
5269+
let _old_auth = env.mock_all_auths_allow_address(other_addr.clone());
5270+
c.set_self_limit(&creator, &500_i128);
5271+
}
5272+
5273+
#[test]
5274+
fn test_creator_self_limit_with_admin_cap() {
5275+
let (env, contract_id, token_id) = setup();
5276+
let c = client(&env, &contract_id);
5277+
let admin = Address::generate(&env);
5278+
let treasury = Address::generate(&env);
5279+
5280+
let creator = Address::generate(&env);
5281+
let recipient = Address::generate(&env);
5282+
5283+
c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &0_u32, &0_u32, &0_u64);
5284+
5285+
StellarAssetClient::new(&env, &token_id).mint(&creator, &1_000_i128);
5286+
env.ledger().set_timestamp(1_000);
5287+
5288+
// Set admin cap to 600
5289+
c.set_creator_volume_cap(&admin, &creator, &600_i128);
5290+
5291+
// Set creator self limit to 400
5292+
c.set_self_limit(&creator, &400_i128);
5293+
5294+
// Create invoice for 300 - should succeed
5295+
let _id1 = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999);
5296+
5297+
// Try to create invoice for 200 - should fail
5298+
// (would exceed self limit of 400, even though admin cap is 600)
5299+
let err_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5300+
make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999)
5301+
}));
5302+
assert!(err_result.is_err(), "Expected panic when exceeding self limit");
5303+
}

contracts/split/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,8 @@ pub struct PenaltyTier {
378378
pub enum TimelockAction {
379379
SetTreasury(Address),
380380
SetPlatformFee(u32),
381+
/// Issue #241: Creator self-imposed limit raise request.
382+
RaiseCreatorSelfLimit(Address, i128),
381383
}
382384

383385
/// A queued timelock action with metadata.

0 commit comments

Comments
 (0)