Skip to content

Commit e4f7814

Browse files
authoredMar 18, 2025
pallet-bounties: allow bounties to never expire (#7723)
# Description Fixes polkadot-fellows/runtimes#509 The Bounties expiration and renewal heavily depends on manual interactions through UI. This PR refactors the duration of the bounty to be an optional configuration constant. If set to None, bounties remain active indefinitely, removing the need for calling`extend_bounty_expiry` and preventing automatic curator slashing for inactivity, which often penalises unnecessarily. ## Integration Remove [BountyUpdatePeriod](https://github.com/polkadot-fellows/runtimes/blob/db4bb534cb411c0d6a2fe57eb331e6ec93ace825/relay/polkadot/src/lib.rs#L774) ## Review Notes Modifies how bounty expiry is handled <details> <summary>🔍 Code Diff Summary</summary> ```diff - #[pallet::constant] - type BountyUpdatePeriod: Get<BlockNumberFor<Self, I>>; + #[pallet::constant] + type BountyUpdatePeriod: Get<Option<BlockNumberFor<Self, I>>>; - *update_due = (Self::treasury_block_number() + T::BountyUpdatePeriod::get()).max(*update_due); + *update_due = Self::treasury_block_number().saturating_add( + T::BountyUpdatePeriod::get().unwrap_or(BlockNumberFor::<T, I>::MAX) + ); ``` </details>
1 parent 35e6bef commit e4f7814

File tree

5 files changed

+126
-17
lines changed

5 files changed

+126
-17
lines changed
 

‎prdoc/pr_7723.prdoc

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
title: '[pallet-bounties] Allow bounties to never expire'
2+
doc:
3+
- audience: Runtime Dev
4+
description: |
5+
Refactored the `update_due` calculation to use `saturating_add`, allowing bounties to remain active
6+
indefinitely without requiring `extend_bounty_expiry` and preventing automatic curator slashing for
7+
inactivity. Previously, setting `BountyUpdatePeriod` to a large value, such as `BlockNumber::max_value()`,
8+
could cause an overflow.
9+
10+
crates:
11+
- name: pallet-bounties
12+
bump: patch
13+
- name: pallet-child-bounties
14+
bump: patch

‎substrate/frame/bounties/src/benchmarking.rs

+15-4
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,26 @@ benchmarks_instance_pallet! {
146146
);
147147
}
148148

149-
// Worst case when curator is inactive and any sender unassigns the curator.
149+
// Worst case when curator is inactive and any sender unassigns the curator,
150+
// or if `BountyUpdatePeriod` is large enough and `RejectOrigin` executes the call.
150151
unassign_curator {
151152
setup_pot_account::<T, I>();
152153
let (curator_lookup, bounty_id) = create_bounty::<T, I>()?;
153154
Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
154155
let bounty_id = BountyCount::<T, I>::get() - 1;
155-
set_block_number::<T, I>(T::SpendPeriod::get() + T::BountyUpdatePeriod::get() + 2u32.into());
156-
let caller = whitelisted_caller();
157-
}: _(RawOrigin::Signed(caller), bounty_id)
156+
let bounty_update_period = T::BountyUpdatePeriod::get();
157+
let inactivity_timeout = T::SpendPeriod::get().saturating_add(bounty_update_period);
158+
set_block_number::<T, I>(inactivity_timeout.saturating_add(2u32.into()));
159+
160+
// If `BountyUpdatePeriod` overflows the inactivity timeout the benchmark still executes the slash
161+
let origin = if Pallet::<T, I>::treasury_block_number() <= inactivity_timeout {
162+
let curator = T::Lookup::lookup(curator_lookup).map_err(<&str>::from)?;
163+
T::RejectOrigin::try_successful_origin().unwrap_or_else(|_| RawOrigin::Signed(curator).into())
164+
} else {
165+
let caller = whitelisted_caller();
166+
RawOrigin::Signed(caller).into()
167+
};
168+
}: _<T::RuntimeOrigin>(origin, bounty_id)
158169

159170
accept_curator {
160171
setup_pot_account::<T, I>();

‎substrate/frame/bounties/src/lib.rs

+11-6
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,12 @@ pub mod pallet {
221221
#[pallet::constant]
222222
type BountyDepositPayoutDelay: Get<BlockNumberFor<Self, I>>;
223223

224-
/// Bounty duration in blocks.
224+
/// The time limit for a curator to act before a bounty expires.
225+
///
226+
/// The period that starts when a curator is approved, during which they must execute or
227+
/// update the bounty via `extend_bounty_expiry`. If missed, the bounty expires, and the
228+
/// curator may be slashed. If `BlockNumberFor::MAX`, bounties stay active indefinitely,
229+
/// removing the need for `extend_bounty_expiry`.
225230
#[pallet::constant]
226231
type BountyUpdatePeriod: Get<BlockNumberFor<Self, I>>;
227232

@@ -578,8 +583,8 @@ pub mod pallet {
578583
T::Currency::reserve(curator, deposit)?;
579584
bounty.curator_deposit = deposit;
580585

581-
let update_due =
582-
Self::treasury_block_number() + T::BountyUpdatePeriod::get();
586+
let update_due = Self::treasury_block_number()
587+
.saturating_add(T::BountyUpdatePeriod::get());
583588
bounty.status =
584589
BountyStatus::Active { curator: curator.clone(), update_due };
585590

@@ -820,9 +825,9 @@ pub mod pallet {
820825
match bounty.status {
821826
BountyStatus::Active { ref curator, ref mut update_due } => {
822827
ensure!(*curator == signer, Error::<T, I>::RequireCurator);
823-
*update_due = (Self::treasury_block_number() +
824-
T::BountyUpdatePeriod::get())
825-
.max(*update_due);
828+
*update_due = Self::treasury_block_number()
829+
.saturating_add(T::BountyUpdatePeriod::get())
830+
.max(*update_due);
826831
},
827832
_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
828833
}

‎substrate/frame/bounties/src/tests.rs

+68-3
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,14 @@ parameter_types! {
137137
pub const CuratorDepositMultiplier: Permill = Permill::from_percent(50);
138138
pub const CuratorDepositMax: Balance = 1_000;
139139
pub const CuratorDepositMin: Balance = 3;
140-
140+
pub static BountyUpdatePeriod: u64 = 20;
141141
}
142142

143143
impl Config for Test {
144144
type RuntimeEvent = RuntimeEvent;
145145
type BountyDepositBase = ConstU64<80>;
146146
type BountyDepositPayoutDelay = ConstU64<3>;
147-
type BountyUpdatePeriod = ConstU64<20>;
147+
type BountyUpdatePeriod = BountyUpdatePeriod;
148148
type CuratorDepositMultiplier = CuratorDepositMultiplier;
149149
type CuratorDepositMax = CuratorDepositMax;
150150
type CuratorDepositMin = CuratorDepositMin;
@@ -160,7 +160,7 @@ impl Config<Instance1> for Test {
160160
type RuntimeEvent = RuntimeEvent;
161161
type BountyDepositBase = ConstU64<80>;
162162
type BountyDepositPayoutDelay = ConstU64<3>;
163-
type BountyUpdatePeriod = ConstU64<20>;
163+
type BountyUpdatePeriod = BountyUpdatePeriod;
164164
type CuratorDepositMultiplier = CuratorDepositMultiplier;
165165
type CuratorDepositMax = CuratorDepositMax;
166166
type CuratorDepositMin = CuratorDepositMin;
@@ -1416,3 +1416,68 @@ fn approve_bounty_with_curator_proposed_unassign_works() {
14161416
assert_eq!(last_event(), BountiesEvent::CuratorUnassigned { bounty_id: 0 });
14171417
});
14181418
}
1419+
1420+
#[test]
1421+
fn accept_curator_sets_update_due_correctly() {
1422+
ExtBuilder::default().build_and_execute(|| {
1423+
// Given (BountyUpdatePeriod = 20)
1424+
let bounty_id = 0;
1425+
let proposer = 0;
1426+
let fee = 10;
1427+
let curator = 4;
1428+
Balances::make_free_balance_be(&Treasury::account_id(), 101);
1429+
Balances::make_free_balance_be(&curator, 12);
1430+
assert_ok!(Bounties::propose_bounty(
1431+
RuntimeOrigin::signed(proposer),
1432+
50,
1433+
b"12345".to_vec()
1434+
));
1435+
assert_ok!(Bounties::approve_bounty(RuntimeOrigin::root(), 0));
1436+
go_to_block(4);
1437+
assert_ok!(Bounties::propose_curator(RuntimeOrigin::root(), bounty_id, curator, fee));
1438+
1439+
// When
1440+
assert_ok!(Bounties::accept_curator(RuntimeOrigin::signed(curator), bounty_id));
1441+
1442+
// Then
1443+
assert_eq!(
1444+
pallet_bounties::Bounties::<Test>::get(bounty_id).unwrap().status,
1445+
BountyStatus::Active { curator, update_due: 24 }
1446+
);
1447+
1448+
// Given (BountyUpdatePeriod = BlockNumber::max_value())
1449+
BountyUpdatePeriod::set(BlockNumberFor::<Test>::max_value());
1450+
Balances::make_free_balance_be(&Treasury1::account_id(), 101);
1451+
assert_ok!(Bounties1::propose_bounty(
1452+
RuntimeOrigin::signed(proposer),
1453+
50,
1454+
b"12345".to_vec()
1455+
));
1456+
assert_ok!(Bounties1::approve_bounty(RuntimeOrigin::root(), bounty_id));
1457+
go_to_block(6);
1458+
<Treasury1 as OnInitialize<u64>>::on_initialize(6);
1459+
assert_ok!(Bounties1::propose_curator(RuntimeOrigin::root(), bounty_id, curator, fee));
1460+
1461+
// When
1462+
assert_ok!(Bounties1::accept_curator(RuntimeOrigin::signed(curator), bounty_id));
1463+
1464+
// Then
1465+
assert_eq!(
1466+
pallet_bounties::Bounties::<Test, Instance1>::get(bounty_id).unwrap().status,
1467+
BountyStatus::Active { curator, update_due: BlockNumberFor::<Test>::max_value() }
1468+
);
1469+
1470+
// When
1471+
assert_ok!(Bounties1::extend_bounty_expiry(
1472+
RuntimeOrigin::signed(curator),
1473+
bounty_id,
1474+
Vec::new()
1475+
));
1476+
1477+
// Then
1478+
assert_eq!(
1479+
pallet_bounties::Bounties::<Test, Instance1>::get(bounty_id).unwrap().status,
1480+
BountyStatus::Active { curator, update_due: BlockNumberFor::<Test>::max_value() }
1481+
);
1482+
});
1483+
}

‎substrate/frame/child-bounties/src/benchmarking.rs

+18-4
Original file line numberDiff line numberDiff line change
@@ -267,17 +267,31 @@ mod benchmarks {
267267
Ok(())
268268
}
269269

270-
// Worst case when curator is inactive and any sender un-assigns the curator.
270+
// Worst case when curator is inactive and any sender un-assigns the curator,
271+
// or if `BountyUpdatePeriod` is large enough and `RejectOrigin` executes the call.
271272
#[benchmark]
272273
fn unassign_curator() -> Result<(), BenchmarkError> {
273274
setup_pot_account::<T>();
274275
let bounty_setup = activate_child_bounty::<T>(0, T::MaximumReasonLength::get())?;
275276
Treasury::<T>::on_initialize(frame_system::Pallet::<T>::block_number());
276-
set_block_number::<T>(T::SpendPeriod::get() + T::BountyUpdatePeriod::get() + 1u32.into());
277-
let caller = whitelisted_caller();
277+
let bounty_update_period = T::BountyUpdatePeriod::get();
278+
let inactivity_timeout = T::SpendPeriod::get().saturating_add(bounty_update_period);
279+
set_block_number::<T>(inactivity_timeout.saturating_add(1u32.into()));
280+
281+
// If `BountyUpdatePeriod` overflows the inactivity timeout the benchmark still
282+
// executes the slash
283+
let origin: T::RuntimeOrigin = if Pallet::<T>::treasury_block_number() <= inactivity_timeout
284+
{
285+
let child_curator = bounty_setup.child_curator;
286+
T::RejectOrigin::try_successful_origin()
287+
.unwrap_or_else(|_| RawOrigin::Signed(child_curator).into())
288+
} else {
289+
let caller = whitelisted_caller();
290+
RawOrigin::Signed(caller).into()
291+
};
278292

279293
#[extrinsic_call]
280-
_(RawOrigin::Signed(caller), bounty_setup.bounty_id, bounty_setup.child_bounty_id);
294+
_(origin as T::RuntimeOrigin, bounty_setup.bounty_id, bounty_setup.child_bounty_id);
281295

282296
Ok(())
283297
}

0 commit comments

Comments
 (0)