Skip to content

Commit b7f2fbb

Browse files
authored
Merge pull request #1591 from opentensor/l0r1s/crowdloan-contributors-limit
Crowdloan max contributors limit to avoid unbounded work
2 parents 0b0dfd1 + 32b124f commit b7f2fbb

File tree

9 files changed

+395
-15
lines changed

9 files changed

+395
-15
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pallets/crowdloan/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ frame-support.workspace = true
2121
frame-system.workspace = true
2222
sp-runtime.workspace = true
2323
sp-std.workspace = true
24+
log = { workspace = true }
2425

2526
[dev-dependencies]
2627
pallet-balances = { default-features = true, workspace = true }
@@ -39,6 +40,7 @@ std = [
3940
"sp-runtime/std",
4041
"sp-std/std",
4142
"sp-io/std",
43+
"log/std",
4244
"sp-core/std",
4345
"pallet-balances/std",
4446
"pallet-preimage/std",

pallets/crowdloan/src/benchmarking.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ mod benchmarks {
6868
target_address: Some(target_address.clone()),
6969
call: Some(T::Preimages::bound(*call).unwrap()),
7070
finalized: false,
71+
contributors_count: 1,
7172
})
7273
);
7374
// ensure the creator has been deducted the deposit

pallets/crowdloan/src/lib.rs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
extern crate alloc;
99

10-
use alloc::{boxed::Box, vec, vec::Vec};
10+
use alloc::{boxed::Box, vec};
1111
use codec::{Decode, Encode};
1212
use frame_support::{
1313
PalletId,
@@ -25,6 +25,7 @@ use frame_support::{
2525
use frame_system::pallet_prelude::*;
2626
use scale_info::TypeInfo;
2727
use sp_runtime::traits::CheckedSub;
28+
use sp_std::vec::Vec;
2829
use weights::WeightInfo;
2930

3031
pub use pallet::*;
@@ -33,6 +34,7 @@ use subtensor_macros::freeze_struct;
3334
pub type CrowdloanId = u32;
3435

3536
mod benchmarking;
37+
mod migrations;
3638
mod mock;
3739
mod tests;
3840
pub mod weights;
@@ -42,11 +44,14 @@ pub type CurrencyOf<T> = <T as Config>::Currency;
4244
pub type BalanceOf<T> =
4345
<CurrencyOf<T> as fungible::Inspect<<T as frame_system::Config>::AccountId>>::Balance;
4446

47+
// Define a maximum length for the migration key
48+
type MigrationKeyMaxLen = ConstU32<128>;
49+
4550
pub type BoundedCallOf<T> =
4651
Bounded<<T as Config>::RuntimeCall, <T as frame_system::Config>::Hashing>;
4752

4853
/// A struct containing the information about a crowdloan.
49-
#[freeze_struct("6b86ccf70fc1b8f1")]
54+
#[freeze_struct("5db9538284491545")]
5055
#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, MaxEncodedLen)]
5156
pub struct CrowdloanInfo<AccountId, Balance, BlockNumber, Call> {
5257
/// The creator of the crowdloan.
@@ -71,6 +76,8 @@ pub struct CrowdloanInfo<AccountId, Balance, BlockNumber, Call> {
7176
pub call: Option<Call>,
7277
/// Whether the crowdloan has been finalized.
7378
pub finalized: bool,
79+
/// The number of contributors to the crowdloan.
80+
pub contributors_count: u32,
7481
}
7582

7683
pub type CrowdloanInfoOf<T> = CrowdloanInfo<
@@ -134,6 +141,10 @@ pub mod pallet {
134141
/// The maximum number of contributors that can be refunded in a single refund.
135142
#[pallet::constant]
136143
type RefundContributorsLimit: Get<u32>;
144+
145+
// The maximum number of contributors that can contribute to a crowdloan.
146+
#[pallet::constant]
147+
type MaxContributors: Get<u32>;
137148
}
138149

139150
/// A map of crowdloan ids to their information.
@@ -162,6 +173,11 @@ pub mod pallet {
162173
#[pallet::storage]
163174
pub type CurrentCrowdloanId<T: Config> = StorageValue<_, CrowdloanId, OptionQuery>;
164175

176+
/// Storage for the migration run status.
177+
#[pallet::storage]
178+
pub type HasMigrationRun<T: Config> =
179+
StorageMap<_, Identity, BoundedVec<u8, MigrationKeyMaxLen>, bool, ValueQuery>;
180+
165181
#[pallet::event]
166182
#[pallet::generate_deposit(pub(super) fn deposit_event)]
167183
pub enum Event<T: Config> {
@@ -253,6 +269,21 @@ pub mod pallet {
253269
NotReadyToDissolve,
254270
/// The deposit cannot be withdrawn from the crowdloan.
255271
DepositCannotBeWithdrawn,
272+
/// The maximum number of contributors has been reached.
273+
MaxContributorsReached,
274+
}
275+
276+
#[pallet::hooks]
277+
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
278+
fn on_runtime_upgrade() -> frame_support::weights::Weight {
279+
let mut weight = frame_support::weights::Weight::from_parts(0, 0);
280+
281+
weight = weight
282+
// Add the contributors count for each crowdloan
283+
.saturating_add(migrations::migrate_add_contributors_count::<T>());
284+
285+
weight
286+
}
256287
}
257288

258289
#[pallet::call]
@@ -342,6 +373,7 @@ pub mod pallet {
342373
target_address,
343374
call,
344375
finalized: false,
376+
contributors_count: 1,
345377
};
346378
Crowdloans::<T>::insert(crowdloan_id, &crowdloan);
347379

@@ -398,6 +430,12 @@ pub mod pallet {
398430
Error::<T>::ContributionTooLow
399431
);
400432

433+
// Ensure the crowdloan has not reached the maximum number of contributors
434+
ensure!(
435+
crowdloan.contributors_count < T::MaxContributors::get(),
436+
Error::<T>::MaxContributorsReached
437+
);
438+
401439
// Ensure contribution does not overflow the actual raised amount
402440
// and it does not exceed the cap
403441
let left_to_raise = crowdloan
@@ -415,11 +453,21 @@ pub mod pallet {
415453
.checked_add(amount)
416454
.ok_or(Error::<T>::Overflow)?;
417455

418-
// Compute the new total contribution and ensure it does not overflow.
419-
let contribution = Contributions::<T>::get(crowdloan_id, &contributor)
420-
.unwrap_or(Zero::zero())
421-
.checked_add(amount)
422-
.ok_or(Error::<T>::Overflow)?;
456+
// Compute the new total contribution and ensure it does not overflow, we
457+
// also increment the contributor count if the contribution is new.
458+
let contribution =
459+
if let Some(contribution) = Contributions::<T>::get(crowdloan_id, &contributor) {
460+
contribution
461+
.checked_add(amount)
462+
.ok_or(Error::<T>::Overflow)?
463+
} else {
464+
// We have a new contribution
465+
crowdloan.contributors_count = crowdloan
466+
.contributors_count
467+
.checked_add(1)
468+
.ok_or(Error::<T>::Overflow)?;
469+
amount
470+
};
423471

424472
// Ensure contributor has enough balance to pay
425473
ensure!(
@@ -476,6 +524,10 @@ pub mod pallet {
476524
Contributions::<T>::insert(crowdloan_id, &who, crowdloan.deposit);
477525
} else {
478526
Contributions::<T>::remove(crowdloan_id, &who);
527+
crowdloan.contributors_count = crowdloan
528+
.contributors_count
529+
.checked_sub(1)
530+
.ok_or(Error::<T>::Underflow)?;
479531
}
480532

481533
CurrencyOf::<T>::transfer(
@@ -625,6 +677,10 @@ pub mod pallet {
625677
refund_count = refund_count.checked_add(1).ok_or(Error::<T>::Overflow)?;
626678
}
627679

680+
crowdloan.contributors_count = crowdloan
681+
.contributors_count
682+
.checked_sub(refund_count)
683+
.ok_or(Error::<T>::Underflow)?;
628684
Crowdloans::<T>::insert(crowdloan_id, &crowdloan);
629685

630686
// Clear refunded contributors
@@ -682,6 +738,7 @@ pub mod pallet {
682738
creator_contribution,
683739
Preservation::Expendable,
684740
)?;
741+
Contributions::<T>::remove(crowdloan_id, &crowdloan.creator);
685742

686743
// Clear the call from the preimage storage
687744
if let Some(call) = crowdloan.call {
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use alloc::string::String;
2+
use frame_support::{BoundedVec, migration::storage_key_iter, traits::Get, weights::Weight};
3+
use subtensor_macros::freeze_struct;
4+
5+
use crate::*;
6+
7+
mod old_storage {
8+
use super::*;
9+
10+
#[freeze_struct("84bcbf9b8d3f0ddf")]
11+
#[derive(Encode, Decode, Debug)]
12+
pub struct OldCrowdloanInfo<AccountId, Balance, BlockNumber, Call> {
13+
pub creator: AccountId,
14+
pub deposit: Balance,
15+
pub min_contribution: Balance,
16+
pub end: BlockNumber,
17+
pub cap: Balance,
18+
pub funds_account: AccountId,
19+
pub raised: Balance,
20+
pub target_address: Option<AccountId>,
21+
pub call: Option<Call>,
22+
pub finalized: bool,
23+
}
24+
}
25+
26+
pub fn migrate_add_contributors_count<T: Config>() -> Weight {
27+
let migration_name = BoundedVec::truncate_from(b"migrate_add_contributors_count".to_vec());
28+
let mut weight = T::DbWeight::get().reads(1);
29+
30+
if HasMigrationRun::<T>::get(&migration_name) {
31+
log::info!(
32+
"Migration '{:?}' has already run. Skipping.",
33+
migration_name
34+
);
35+
return weight;
36+
}
37+
38+
log::info!(
39+
"Running migration '{}'",
40+
String::from_utf8_lossy(&migration_name)
41+
);
42+
43+
let pallet_name = b"Crowdloan";
44+
let item_name = b"Crowdloans";
45+
let crowdloans = storage_key_iter::<
46+
CrowdloanId,
47+
old_storage::OldCrowdloanInfo<
48+
T::AccountId,
49+
BalanceOf<T>,
50+
BlockNumberFor<T>,
51+
BoundedCallOf<T>,
52+
>,
53+
Twox64Concat,
54+
>(pallet_name, item_name)
55+
.collect::<Vec<_>>();
56+
weight = weight.saturating_add(T::DbWeight::get().reads(crowdloans.len() as u64));
57+
58+
for (id, crowdloan) in crowdloans {
59+
let contributions = Contributions::<T>::iter_key_prefix(id)
60+
.collect::<Vec<_>>()
61+
.len();
62+
weight = weight.saturating_add(T::DbWeight::get().reads(contributions as u64));
63+
64+
Crowdloans::<T>::insert(
65+
id,
66+
CrowdloanInfo {
67+
creator: crowdloan.creator,
68+
deposit: crowdloan.deposit,
69+
min_contribution: crowdloan.min_contribution,
70+
end: crowdloan.end,
71+
cap: crowdloan.cap,
72+
funds_account: crowdloan.funds_account,
73+
raised: crowdloan.raised,
74+
target_address: crowdloan.target_address,
75+
call: crowdloan.call,
76+
finalized: crowdloan.finalized,
77+
contributors_count: contributions as u32,
78+
},
79+
);
80+
weight = weight.saturating_add(T::DbWeight::get().writes(1));
81+
}
82+
83+
HasMigrationRun::<T>::insert(&migration_name, true);
84+
weight = weight.saturating_add(T::DbWeight::get().writes(1));
85+
86+
log::info!(
87+
"Migration '{:?}' completed successfully.",
88+
String::from_utf8_lossy(&migration_name)
89+
);
90+
91+
weight
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use frame_support::{Hashable, storage::unhashed::put_raw};
97+
use sp_core::U256;
98+
use sp_io::hashing::twox_128;
99+
100+
use super::*;
101+
use crate::mock::{Test, TestState};
102+
103+
#[test]
104+
fn test_migrate_add_contributors_count_works() {
105+
TestState::default().build_and_execute(|| {
106+
let pallet_name = twox_128(b"Crowdloan");
107+
let storage_name = twox_128(b"Crowdloans");
108+
let prefix = [pallet_name, storage_name].concat();
109+
110+
let items = vec![
111+
(
112+
old_storage::OldCrowdloanInfo {
113+
creator: U256::from(1),
114+
deposit: 100u64,
115+
min_contribution: 10u64,
116+
end: 100u64,
117+
cap: 1000u64,
118+
funds_account: U256::from(2),
119+
raised: 0u64,
120+
target_address: None,
121+
call: None::<BoundedCallOf<Test>>,
122+
finalized: false,
123+
},
124+
vec![(U256::from(1), 100)],
125+
),
126+
(
127+
old_storage::OldCrowdloanInfo {
128+
creator: U256::from(1),
129+
deposit: 100u64,
130+
min_contribution: 10u64,
131+
end: 100u64,
132+
cap: 1000u64,
133+
funds_account: U256::from(2),
134+
raised: 0u64,
135+
target_address: None,
136+
call: None::<BoundedCallOf<Test>>,
137+
finalized: false,
138+
},
139+
vec![
140+
(U256::from(1), 100),
141+
(U256::from(2), 100),
142+
(U256::from(3), 100),
143+
],
144+
),
145+
(
146+
old_storage::OldCrowdloanInfo {
147+
creator: U256::from(1),
148+
deposit: 100u64,
149+
min_contribution: 10u64,
150+
end: 100u64,
151+
cap: 1000u64,
152+
funds_account: U256::from(2),
153+
raised: 0u64,
154+
target_address: None,
155+
call: None::<BoundedCallOf<Test>>,
156+
finalized: false,
157+
},
158+
vec![
159+
(U256::from(1), 100),
160+
(U256::from(2), 100),
161+
(U256::from(3), 100),
162+
(U256::from(4), 100),
163+
(U256::from(5), 100),
164+
],
165+
),
166+
];
167+
168+
for (id, (crowdloan, contributions)) in items.into_iter().enumerate() {
169+
let key = [prefix.clone(), (id as u32).twox_64_concat()].concat();
170+
put_raw(&key, &crowdloan.encode());
171+
172+
for (contributor, amount) in contributions {
173+
Contributions::<Test>::insert(id as u32, contributor, amount);
174+
}
175+
}
176+
177+
migrate_add_contributors_count::<Test>();
178+
179+
assert!(Crowdloans::<Test>::get(0).is_some_and(|c| c.contributors_count == 1));
180+
assert!(Crowdloans::<Test>::get(1).is_some_and(|c| c.contributors_count == 3));
181+
assert!(Crowdloans::<Test>::get(2).is_some_and(|c| c.contributors_count == 5));
182+
183+
assert!(HasMigrationRun::<Test>::get(BoundedVec::truncate_from(
184+
b"migrate_add_contributors_count".to_vec()
185+
)));
186+
});
187+
}
188+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mod migrate_add_contributors_count;
2+
pub use migrate_add_contributors_count::*;

0 commit comments

Comments
 (0)