Skip to content
Open
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
12 changes: 10 additions & 2 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ pub const BPS_DENOMINATOR: i128 = 10_000;
/// accumulate. Bounds the per-contributor loops in `refund` and
/// `extend_deadline` to a small, predictable constant regardless of how
/// popular a bounty gets. See `docs/escrow-crowdfunding-design.md`.
pub const MAX_SPONSORS: u32 = 20;
/// Default maximum number of sponsors if not configured at initialization.
pub const DEFAULT_MAX_SPONSORS: u32 = 20;

/// Minimum grace period (in seconds) after the deadline before anyone can permissionlessly trigger a refund.
/// This prevents a race condition where a legitimate release in-flight near the deadline gets front-run by a refund.
Expand Down Expand Up @@ -52,6 +53,7 @@ impl EscrowContract {
admin: Address,
treasury: Address,
fee_bps: u32,
max_sponsors: Option<u32>,
) -> Result<(), Error> {
admin.require_auth();

Expand All @@ -65,6 +67,11 @@ impl EscrowContract {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Treasury, &treasury);
env.storage().instance().set(&DataKey::FeeBps, &fee_bps);
let limit = max_sponsors.unwrap_or(DEFAULT_MAX_SPONSORS);
if limit == 0 {
return Err(Error::InvalidAmount);
}
env.storage().instance().set(&DataKey::MaxSponsors, &limit);
Ok(())
}

Expand Down Expand Up @@ -158,7 +165,8 @@ impl EscrowContract {
EscrowStatus::Funded => {}
}

if escrow.contributor_count >= MAX_SPONSORS {
let max_sponsors: u32 = env.storage().instance().get(&DataKey::MaxSponsors).unwrap_or(DEFAULT_MAX_SPONSORS);
if escrow.contributor_count >= max_sponsors {
return Err(Error::TooManySponsors);
}

Expand Down
14 changes: 7 additions & 7 deletions contracts/escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn setup(env: &Env) -> (Address, Address, Address, EscrowContractClient<'_>) {
let treasury = Address::generate(env);
let contract_id = env.register(EscrowContract, ());
let client = EscrowContractClient::new(env, &contract_id);
client.initialize(&admin, &treasury, &500u32); // 5% fee
client.initialize(&admin, &treasury, &500u32, &None); // 5% fee
(contract_id, admin, treasury, client)
}

Expand All @@ -33,7 +33,7 @@ fn test_initialize_rejects_double_init() {
let env = Env::default();
env.mock_all_auths();
let (_, admin, treasury, client) = setup(&env);
let err = client.try_initialize(&admin, &treasury, &500u32);
let err = client.try_initialize(&admin, &treasury, &500u32, &None);
assert_eq!(err, Err(Ok(Error::AlreadyInitialized)));
}

Expand Down Expand Up @@ -247,7 +247,7 @@ fn test_adversarial_ordering_resistance() {
let client = crate::EscrowContractClient::new(&env, &contract_id);

// Initialize with 0% fee to simplify fraction/dust calculations
client.initialize(&admin, &treasury, &0u32);
client.initialize(&admin, &treasury, &0u32, &None);

// 2. Create recipient addresses
let dev1 = Address::generate(&env);
Expand Down Expand Up @@ -312,7 +312,7 @@ fn test_large_split_distributes_dust_by_largest_remainder() {
let contract_id = env.register(crate::EscrowContract, ());
let client = crate::EscrowContractClient::new(&env, &contract_id);
// 0% fee so the whole total is distributable.
client.initialize(&admin, &treasury, &0u32);
client.initialize(&admin, &treasury, &0u32, &None);

// 60 recipients: 59 with alternating 160/170 bps, the last one receiving
// the leftover of 10000. All 170-bps recipients share an identical
Expand Down Expand Up @@ -400,7 +400,7 @@ fn test_initialize_requires_admin_auth() {
let contract_id = env.register(EscrowContract, ());
let client = EscrowContractClient::new(&env, &contract_id);

let result = client.try_initialize(&admin, &treasury, &500u32);
let result = client.try_initialize(&admin, &treasury, &500u32, &None);
assert!(result.is_err());
}

Expand Down Expand Up @@ -732,14 +732,14 @@ fn test_contribute_rejects_beyond_max_sponsors() {

// MAX_SPONSORS is 20; alice's `fund` call above already used slot 0, so
// 19 more `contribute` calls exactly fill the cap.
for _ in 0..(crate::MAX_SPONSORS - 1) {
for _ in 0..(crate::DEFAULT_MAX_SPONSORS - 1) {
let extra = Address::generate(&env);
asset_client.mint(&extra, &1_000i128);
client.contribute(&105u64, &extra, &1_000i128);
}
assert_eq!(
client.get_escrow(&105u64).contributor_count,
crate::MAX_SPONSORS
crate::DEFAULT_MAX_SPONSORS
);

// The 21st distinct contribution is rejected.
Expand Down
1 change: 1 addition & 0 deletions contracts/escrow/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub enum DataKey {
Admin,
Treasury,
FeeBps,
MaxSponsors,
Escrow(u64),
Contribution(u64, u32), // (issue_id, contribution_index)
}
Expand Down
12 changes: 10 additions & 2 deletions contracts/milestones/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub const BPS_DENOMINATOR: i128 = 10_000;
/// (and any future timeout-triggered wind-down that reuses
/// `refund_remaining_budget`) to a small, predictable constant regardless
/// of how popular a release gets. See `docs/milestones-crowdfunding-design.md`.
pub const MAX_SPONSORS: u32 = 20;
/// Default maximum number of sponsors if not configured at initialization.
pub const DEFAULT_MAX_SPONSORS: u32 = 20;

#[contract]
pub struct MilestonesContract;
Expand All @@ -44,6 +45,7 @@ impl MilestonesContract {
admin: Address,
treasury: Address,
fee_bps: u32,
max_sponsors: Option<u32>,
) -> Result<(), Error> {
admin.require_auth();

Expand All @@ -56,6 +58,11 @@ impl MilestonesContract {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Treasury, &treasury);
env.storage().instance().set(&DataKey::FeeBps, &fee_bps);
let limit = max_sponsors.unwrap_or(DEFAULT_MAX_SPONSORS);
if limit == 0 {
return Err(Error::InvalidAmount);
}
env.storage().instance().set(&DataKey::MaxSponsors, &limit);
Ok(())
}

Expand Down Expand Up @@ -144,7 +151,8 @@ impl MilestonesContract {
if milestone.closed {
return Err(Error::MilestoneClosed);
}
if milestone.contributor_count >= MAX_SPONSORS {
let max_sponsors: u32 = env.storage().instance().get(&DataKey::MaxSponsors).unwrap_or(DEFAULT_MAX_SPONSORS);
if milestone.contributor_count >= max_sponsors {
return Err(Error::TooManySponsors);
}

Expand Down
10 changes: 5 additions & 5 deletions contracts/milestones/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn setup(env: &Env) -> (Address, Address, MilestonesContractClient<'_>) {
let treasury = Address::generate(env);
let contract_id = env.register(MilestonesContract, ());
let client = MilestonesContractClient::new(env, &contract_id);
client.initialize(&admin, &treasury, &500u32); // 5% fee
client.initialize(&admin, &treasury, &500u32, &None); // 5% fee
(admin, treasury, client)
}

Expand Down Expand Up @@ -109,7 +109,7 @@ fn test_large_split_distributes_dust_by_largest_remainder() {
let contract_id = env.register(crate::MilestonesContract, ());
let client = crate::MilestonesContractClient::new(&env, &contract_id);
// 0% fee so the whole total is distributable.
client.initialize(&admin, &treasury, &0u32);
client.initialize(&admin, &treasury, &0u32, &None);

// 60 recipients: 59 with alternating 160/170 bps, the last one receiving
// the leftover of 10000. All 170-bps recipients share an identical
Expand Down Expand Up @@ -261,7 +261,7 @@ fn test_initialize_requires_admin_auth() {
let contract_id = env.register(MilestonesContract, ());
let client = MilestonesContractClient::new(&env, &contract_id);

let result = client.try_initialize(&admin, &treasury, &500u32);
let result = client.try_initialize(&admin, &treasury, &500u32, &None);
assert!(result.is_err());
}

Expand Down Expand Up @@ -516,14 +516,14 @@ fn test_contribute_rejects_beyond_max_sponsors() {

// MAX_SPONSORS is 20; alice's `create_milestone` above already used
// slot 0, so 19 more `contribute` calls exactly fill the cap.
for _ in 0..(crate::MAX_SPONSORS - 1) {
for _ in 0..(crate::DEFAULT_MAX_SPONSORS - 1) {
let extra = Address::generate(&env);
asset_client.mint(&extra, &1_000i128);
client.contribute(&56u64, &extra, &1_000i128);
}
assert_eq!(
client.get_milestone(&56u64).contributor_count,
crate::MAX_SPONSORS
crate::DEFAULT_MAX_SPONSORS
);

// The 21st distinct contribution is rejected.
Expand Down
1 change: 1 addition & 0 deletions contracts/milestones/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub enum DataKey {
Admin,
Treasury,
FeeBps,
MaxSponsors,
Milestone(u64),
IssueStatus(u64, u64), // (milestone_id, issue_id)
Contribution(u64, u32), // (milestone_id, contribution_index)
Expand Down