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
2 changes: 1 addition & 1 deletion pallets/mining-rewards/src/lib.rs
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to my changes, but we had a Clippy warning here, so I'm fixing this as well.

Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub mod pallet {

let total_reward = remaining_supply
.checked_div(&emission_divisor)
.unwrap_or_else(|| BalanceOf::<T>::zero());
.unwrap_or_else(BalanceOf::<T>::zero);

// Split the reward between treasury and miner
let treasury_portion = T::TreasuryPortion::get();
Expand Down
45 changes: 37 additions & 8 deletions pallets/multisig/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ Before creating a new proposal, the system **automatically removes all expired A

This ensures storage is kept clean and users get their deposits back without manual intervention.

**Threshold=1 Auto-Execution:**
If the multisig has `threshold=1`, the proposal **executes immediately** after creation:
- Proposer's approval counts as the first (and only required) approval
- Call is dispatched automatically
- Proposal is removed from storage immediately
- Deposit is returned to proposer immediately
- No separate `approve()` call needed

**Economic Costs:**
- **ProposalFee**: Non-refundable fee (spam prevention, scaled by signer count) → burned
- **ProposalDeposit**: Refundable deposit (storage rent) → returned when proposal removed
Expand Down Expand Up @@ -129,7 +137,7 @@ Cancels a proposal and immediately removes it from storage (proposer only).
### 5. Remove Expired
Manually removes expired proposals from storage. Only signers can call this.

**Important:** This is rarely needed because expired proposals are automatically cleaned up when anyone creates a new proposal in the same multisig.
**Important:** This is rarely needed because expired proposals are automatically cleaned up on any multisig activity (`propose()`, `approve()`, `cancel()`).

**Required Parameters:**
- `multisig_address: AccountId` - Target multisig (REQUIRED)
Expand All @@ -149,12 +157,12 @@ Manually removes expired proposals from storage. Only signers can call this.

**Economic Costs:** None (deposit always returned to proposer)

**Auto-Cleanup:** When anyone calls `propose()`, all expired proposals are automatically removed first, making this function often unnecessary.
**Auto-Cleanup:** ALL expired proposals are automatically removed on any multisig activity (`propose()`, `approve()`, `cancel()`), making this function often unnecessary.

### 6. Claim Deposits
Batch cleanup operation to recover all expired proposal deposits.

**Important:** This is rarely needed because expired proposals are automatically cleaned up when anyone creates a new proposal in the same multisig.
**Important:** This is rarely needed because expired proposals are automatically cleaned up on any multisig activity (`propose()`, `approve()`, `cancel()`).

**Required Parameters:**
- `multisig_address: AccountId` - Target multisig (REQUIRED)
Expand All @@ -171,7 +179,27 @@ Batch cleanup operation to recover all expired proposal deposits.

**Economic Costs:** None (only returns deposits)

**Auto-Cleanup:** When anyone calls `propose()`, all expired proposals are automatically removed first, making this function often unnecessary.
**Auto-Cleanup:** ALL expired proposals are automatically removed on any multisig activity (`propose()`, `approve()`, `cancel()`), making this function often unnecessary.

### 7. Dissolve Multisig
Permanently removes a multisig and returns the creation deposit to the original creator.

**Required Parameters:**
- `multisig_address: AccountId` - Target multisig (REQUIRED)

**Pre-conditions:**
- NO proposals can exist (any status)
- Multisig balance MUST be zero
- Caller must be creator OR any signer

**Post-conditions:**
- MultisigDeposit returned to **original creator** (not caller)
- Multisig removed from storage
- Cannot be used after dissolution

**Economic Costs:** None (returns MultisigDeposit)

**Important:** MultisigFee is NEVER returned - only the MultisigDeposit.

## Use Cases

Expand Down Expand Up @@ -228,10 +256,11 @@ matches!(call,
- **Auto-Returned Immediately:**
- When proposal executed (threshold reached)
- When proposal cancelled (proposer cancels)
- **Manual Cleanup Required:**
- Expired proposals: Must be manually removed OR auto-cleaned on next `propose()`
- **Auto-Cleanup:** When anyone creates new proposal, all expired proposals cleaned automatically
- No grace period needed - executed/cancelled proposals auto-removed
- **Auto-Cleanup:** ALL expired proposals are automatically removed on ANY multisig activity
- Triggered by: `propose()`, `approve()`, `cancel()`
- Deposits returned to original proposers
- No manual cleanup needed for active multisigs
- **Manual Cleanup:** Only needed for inactive multisigs via `remove_expired()` or `claim_deposits()`

### Storage Limits & Configuration
**Purpose:** Prevent unbounded storage growth and resource exhaustion
Expand Down
78 changes: 63 additions & 15 deletions pallets/multisig/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,19 @@ mod benchmarks {
#[benchmark]
fn approve(
c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>,
e: Linear<0, { T::MaxTotalProposalsInStorage::get() }>, // expired proposals to cleanup
) -> Result<(), BenchmarkError> {
// Setup: Create multisig and proposal directly in storage
// Threshold is 3, so adding one more approval won't trigger execution
let caller: T::AccountId = whitelisted_caller();
fund_account::<T>(&caller, BalanceOf2::<T>::from(10000u128));
fund_account::<T>(&caller, BalanceOf2::<T>::from(100000u128));

let signer1: T::AccountId = benchmark_account("signer1", 0, SEED);
let signer2: T::AccountId = benchmark_account("signer2", 1, SEED);
let signer3: T::AccountId = benchmark_account("signer3", 2, SEED);
fund_account::<T>(&signer1, BalanceOf2::<T>::from(10000u128));
fund_account::<T>(&signer2, BalanceOf2::<T>::from(10000u128));
fund_account::<T>(&signer3, BalanceOf2::<T>::from(10000u128));
fund_account::<T>(&signer1, BalanceOf2::<T>::from(100000u128));
fund_account::<T>(&signer2, BalanceOf2::<T>::from(100000u128));
fund_account::<T>(&signer3, BalanceOf2::<T>::from(100000u128));

let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone(), signer3.clone()];
let threshold = 3u32; // Need 3 approvals
Expand All @@ -159,16 +160,39 @@ mod benchmarks {
signers: bounded_signers,
threshold,
nonce: 0,
proposal_nonce: 1, // We'll insert proposal with id 0
proposal_nonce: e + 1, // We'll insert e expired proposals + 1 active
creator: caller.clone(),
deposit: T::MultisigDeposit::get(),
last_activity: frame_system::Pallet::<T>::block_number(),
active_proposals: 1,
active_proposals: e + 1,
proposals_per_signer: BoundedBTreeMap::new(),
};
Multisigs::<T>::insert(&multisig_address, multisig_data);

// Directly insert proposal into storage with 1 approval
// Insert e expired proposals (worst case for auto-cleanup)
let expired_block = 10u32.into();
for i in 0..e {
let system_call = frame_system::Call::<T>::remark { remark: vec![i as u8; 10] };
let call = <T as Config>::RuntimeCall::from(system_call);
let encoded_call = call.encode();
let bounded_call: BoundedCallOf<T> = encoded_call.try_into().unwrap();
let bounded_approvals: BoundedApprovalsOf<T> = vec![caller.clone()].try_into().unwrap();

let proposal_data = ProposalDataOf::<T> {
proposer: caller.clone(),
call: bounded_call,
expiry: expired_block,
approvals: bounded_approvals,
deposit: 10u32.into(),
status: ProposalStatus::Active,
};
Proposals::<T>::insert(&multisig_address, i, proposal_data);
}

// Move past expiry so proposals are expired
frame_system::Pallet::<T>::set_block_number(100u32.into());

// Directly insert active proposal into storage with 1 approval
// Create a remark call where the remark itself is c bytes
let system_call = frame_system::Call::<T>::remark { remark: vec![1u8; c as usize] };
let call = <T as Config>::RuntimeCall::from(system_call);
Expand All @@ -186,7 +210,7 @@ mod benchmarks {
status: ProposalStatus::Active,
};

let proposal_id = 0u32;
let proposal_id = e; // Active proposal after expired ones
Proposals::<T>::insert(&multisig_address, proposal_id, proposal_data);

#[extrinsic_call]
Expand Down Expand Up @@ -271,15 +295,16 @@ mod benchmarks {
#[benchmark]
fn cancel(
c: Linear<0, { T::MaxCallSize::get().saturating_sub(100) }>,
e: Linear<0, { T::MaxTotalProposalsInStorage::get() }>, // expired proposals to cleanup
) -> Result<(), BenchmarkError> {
// Setup: Create multisig and proposal directly in storage
let caller: T::AccountId = whitelisted_caller();
fund_account::<T>(&caller, BalanceOf2::<T>::from(10000u128));
fund_account::<T>(&caller, BalanceOf2::<T>::from(100000u128));

let signer1: T::AccountId = benchmark_account("signer1", 0, SEED);
let signer2: T::AccountId = benchmark_account("signer2", 1, SEED);
fund_account::<T>(&signer1, BalanceOf2::<T>::from(10000u128));
fund_account::<T>(&signer2, BalanceOf2::<T>::from(10000u128));
fund_account::<T>(&signer1, BalanceOf2::<T>::from(100000u128));
fund_account::<T>(&signer2, BalanceOf2::<T>::from(100000u128));

let mut signers = vec![caller.clone(), signer1.clone(), signer2.clone()];
let threshold = 2u32;
Expand All @@ -294,16 +319,39 @@ mod benchmarks {
signers: bounded_signers,
threshold,
nonce: 0,
proposal_nonce: 1, // We'll insert proposal with id 0
proposal_nonce: e + 1, // We'll insert e expired proposals + 1 active
creator: caller.clone(),
deposit: T::MultisigDeposit::get(),
last_activity: frame_system::Pallet::<T>::block_number(),
active_proposals: 1,
active_proposals: e + 1,
proposals_per_signer: BoundedBTreeMap::new(),
};
Multisigs::<T>::insert(&multisig_address, multisig_data);

// Directly insert proposal into storage
// Insert e expired proposals (worst case for auto-cleanup)
let expired_block = 10u32.into();
for i in 0..e {
let system_call = frame_system::Call::<T>::remark { remark: vec![i as u8; 10] };
let call = <T as Config>::RuntimeCall::from(system_call);
let encoded_call = call.encode();
let bounded_call: BoundedCallOf<T> = encoded_call.try_into().unwrap();
let bounded_approvals: BoundedApprovalsOf<T> = vec![caller.clone()].try_into().unwrap();

let proposal_data = ProposalDataOf::<T> {
proposer: caller.clone(),
call: bounded_call,
expiry: expired_block,
approvals: bounded_approvals,
deposit: 10u32.into(),
status: ProposalStatus::Active,
};
Proposals::<T>::insert(&multisig_address, i, proposal_data);
}

// Move past expiry so proposals are expired
frame_system::Pallet::<T>::set_block_number(100u32.into());

// Directly insert active proposal into storage
// Create a remark call where the remark itself is c bytes
let system_call = frame_system::Call::<T>::remark { remark: vec![1u8; c as usize] };
let call = <T as Config>::RuntimeCall::from(system_call);
Expand All @@ -321,7 +369,7 @@ mod benchmarks {
status: ProposalStatus::Active,
};

let proposal_id = 0u32;
let proposal_id = e; // Active proposal after expired ones
Proposals::<T>::insert(&multisig_address, proposal_id, proposal_data);

#[extrinsic_call]
Expand Down
Loading
Loading