The slashing mechanism is a governance-controlled penalty system that reduces a bond's value as punishment for misconduct, breach of obligations, or violations of protocol rules. Slashed funds represent a loss to the bonded identity while maintaining an accurate record for auditing and transparency.
Slashing = Reducing bonded_amount availability by incrementing slashed_amount
- Bonded Amount: Total stake locked in the bond (i128)
- Slashed Amount: Cumulative penalty (i128)
- Available Balance:
bonded_amount - slashed_amount— the only amount that can be slashed or withdrawn - Withdrawable Balance: same as Available Balance
- Monotonic: Slashing only increases, never decreases (unless unslashing by admin)
- Fair: Prevents over-slashing — slash requests above the remaining available balance (
bonded - slashed) are rejected - Transparent: Events emit for all slashing operations; every slash is recorded in persistent history
- Accountable: Only authorized governance can execute
The slash_bond() function can only be called by the contract admin:
Admin: Address stored at contract initialization
Caller: Must equal the stored admin address
Rejection: "not admin" panic if unauthorized
- ✅ Non-transferable: Admin role cannot be changed after initialization (in this version)
- ✅ Non-delegable: Admin must directly call slashing (no proxies)
- ✅ Auditable: All slashing events are logged on-chain
Core slashing function.
Behavior:
- Validates caller is the contract admin (panics if not)
- Computes available balance =
bonded_amount - slashed_amount - Rejects zero or negative slash amounts
- Rejects slash requests above available balance
- Updates bond state with new
slashed_amountusing checked arithmetic - Appends a normalized
SlashRecordto persistent slash history - Emits
bond_slashedevent - Returns updated
IdentityBondstruct
Arguments:
admin: Address- Caller claiming admin authorityamount: i128- Positive amount to slash
Returns:
IdentityBondwith updatedslashed_amount
Panics:
"not admin"if caller is not the contract admin"no bond"if no bond exists"slash amount must be positive"ifamount <= 0"slash exceeds bond"if the request would makeslashed_amount > bonded_amount"slashing caused overflow"if arithmetic overflows (unreachable in practice due to available-balance cap)
Example:
// Admin slashes 300 from a 1000-unit bond (0 previously slashed)
let bond = contract.slash(admin_address, 300);
// bond.slashed_amount == 300
// bond.bonded_amount == 1000 (unchanged)
// available_balance == 700Partial Slash:
Slash amount < bonded amount, leaving some withdrawable balance
Bonded: 1000
Slash: 300
Available: 1000 - 300 = 700
Full Slash:
Slash amount >= bonded amount, leaving zero withdrawable balance
Bonded: 1000
Slash: 1000
Available: 1000 - 1000 = 0
Slash is bounded by the available balance (bonded - slashed), not just bonded_amount. A second slash cannot exceed what is actually withdrawable:
Bonded: 1000
Previous Slash: 700
Available: 1000 - 700 = 300
New Slash Request: 500
Result: rejected with "slash exceeds bond"
Final Slashed: 700
This is stricter than capping at bonded_amount alone and prevents any scenario where slashed_amount could exceed bonded_amount.
Every successful call to slash_bond() appends a normalized SlashRecord to persistent storage, keyed by identity address and index.
pub struct SlashRecord {
pub identity: Address, // Slashed identity
pub slash_amount: i128, // Validated amount slashed
pub reason: Symbol, // "admin_slash"
pub timestamp: u64, // Ledger timestamp at slash time
pub total_slashed_after: i128,// Cumulative slashed_amount after this slash
}// Number of slash records for an identity
get_slash_count(e, identity) -> u32
// All records for an identity (ordered by index)
get_slash_history(e, identity) -> Vec<SlashRecord>
// Single record by index
get_slash_record(e, identity, index) -> SlashRecordslash_amountin the record is the validated amount applied to the bond.- Records are stored in
persistentstorage and survive ledger TTL extensions. - Zero and negative slash amounts are rejected and do not append records.
pub struct IdentityBond {
pub identity: Address, // Bonded identity
pub bonded_amount: i128, // Total stake (unchanged by slashing)
pub slashed_amount: i128, // Cumulative penalties
pub bond_start: u64, // Timestamp of bond creation
pub bond_duration: u64, // Lock-up duration
pub active: bool, // Is bond active
pub is_rolling: bool, // Auto-renew at end
pub withdrawal_requested_at: u64, // Rolling bond withdrawal request time
pub notice_period_duration: u64, // Rolling bond notice period
}Withdrawal Logic:
available_balance = bonded_amount - slashed_amount;
if withdraw_amount > available_balance {
panic!("insufficient balance for withdrawal")
}Examples:
| Bonded | Slashed | Available | Withdraw | Result |
|---|---|---|---|---|
| 1000 | 300 | 700 | 500 | ✅ OK |
| 1000 | 300 | 700 | 701 | ❌ Panic |
| 1000 | 1000 | 0 | 1 | ❌ Panic |
Emitted whenever a bond is successfully slashed.
Event Data:
(Symbol: "bond_slashed")
- identity: Address of the slashed identity
- slash_amount: Amount just slashed (i128)
- total_slashed_amount: New cumulative slashed amount (i128)
Audit Trail Value:
- Off-chain indexing: Find all slashing events for an identity
- Transparency: Public record of governance actions
- Analytics: Track slashing patterns and severity
// Initial bond: 1000 units
client.create_bond(identity, 1000, ...);
// First slash: 300 units
client.slash(admin, 300);
// Event: (identity, 300, 300)
// Second slash: 200 units
client.slash(admin, 200);
// Event: (identity, 200, 500)
// Attempt third slash: 600 units (would exceed 1000)
client.slash(admin, 600);
// Reverts: "slash exceeds bond"✅ Admin Validation:
// Rejects non-admin with "not admin" panic
validate_admin(e, caller);✅ State Consistency:
- Bond must exist (panics if not)
- No state corruption on failed slash
✅ Overflow Protection:
let new_slashed = bond.slashed_amount
.checked_add(amount)
.expect("slashing caused overflow");✅ Over-Slash Prevention (available-balance bound):
if new_slashed > bond.bonded_amount {
panic!("slash exceeds bond");
}✅ Atomic Updates:
- Slash calculation verified before state update
- All validations before persist
✅ No Partial States:
- Bond either slashed completely or not at all
- Event only emitted on success
✅ Available Balance Calculation:
available = bonded_amount - slashed_amount
// Always verified >= withdrawal_amount✅ Never Over-Withdraw:
- Slashing reduces available balance
- Withdrawal checks always pass with correct available calculation
-
Basic Operations (4 tests)
- Successful slash execution
- Small amount slashing
- Exact half slashing
- Full amount slashing
-
Authorization (3 tests)
- Unauthorized rejection
- Multiple unauthorized attempts
- Identity cannot slash own bond
-
Over-Slash Prevention (3 tests)
- Amount exceeds bonded
- Way over amount
- Max i128 value rejection
-
Edge Cases (3 tests)
- Zero amount rejection
- Negative amount rejection
- Overflow prevention
- Very large bonds (i128::MAX / 2)
-
State Consistency (5 tests)
- Single slash recording
- Cumulative slashing
- Multiple accumulation
- Other fields unchanged
- State persistence
-
Event Emission (3 tests)
- Event emitted on basic slash
- Correct event data
- Multiple events
-
Withdrawal Integration (5 tests)
- Withdraw respects available balance
- Over-withdrawal prevention
- Fully slashed bonds cannot withdraw
- Exact available balance withdrawal
- Complex slash/withdraw sequences
-
Cumulative Scenarios (5 tests)
- Cumulative over-cap rejection
- Incremental slashing
- Full slash prevents further slashing
- Large amount accumulation
-
State Persistence (2 tests)
- State persists across calls
- Slash result matches get_state
-
Error Messages (2 tests)
- "not admin" error
- "no bond" error
-
Available-Balance Bound (4 tests)
- Slash rejected above available (not bonded) after partial slash
- Zero-available rejects further slashing
- Available decreases after each slash
- Slash after withdrawal respects new available
-
Slash History Records (6 tests)
- Count increments per slash
- Record fields (identity, amount, timestamp, total_slashed_after)
- Cumulative total_slashed_after
- Capped slash records actual amount
- Zero slash appends record
- Full history retrieval
// Admin slashes 10% of bond for minor violation
let bond = contract.slash(admin, 100);
// slashed_amount increases from 0 to 100
// bonded_amount remains 1000
// withdrawable becomes 900// First offense: 5%
contract.slash(admin, 50);
// slashed_amount = 50
// Second offense: 10%
contract.slash(admin, 100);
// slashed_amount = 150 (cumulative)
// Third offense: 20%
contract.slash(admin, 200);
// slashed_amount = 350 (if bonded >= 350)// Severe violation: slash the remaining bond exactly
let bond = contract.slash(admin, 1000);
// slashed_amount equals bonded_amount (1000)
// bonded_amount remains 1000
// withdrawable = 0
// Identity cannot withdrawlet bond = contract.create_bond(identity, 1000, ...);
// Slash 300
contract.slash(admin, 300);
// available = 1000 - 300 = 700
// Withdraw 500 (less than available)
contract.withdraw(500);
// bonded_amount = 500, slashed_amount = 300, available = 200
// Try to withdraw 300 (more than available)
contract.withdraw(300);
// panics: "insufficient balance for withdrawal"- Early Exit: Charged to users, transferred to treasury, applies at withdrawal time
- Slashing: Imposed by governance, tracked in bond state, affects available balance
- Top-Up: Increases bonded_amount (additive)
- Slashing: Increases slashed_amount (subtractive, can't be reversed without unslashing)
- Withdrawal: Reduces bonded_amount (removes funds)
- Slashing: Increases slashed_amount (blocks funds without removing)
- Partial Unslashing: Allow admin to reduce slashed_amount for appeals
- Treasury Integration: Actual fund transfers to governance treasury
- Slashing Tiers: Different slash amounts based on violation severity
- Timelocks: Delay slash execution for governance safety
- Signaling: Allow other addresses to propose slashing for governance review
Slashed funds are not transferred to the treasury in this reference implementation. See known-simplifications.md for details and the production path.