Skip to content

Latest commit

 

History

History
439 lines (328 loc) · 12.3 KB

File metadata and controls

439 lines (328 loc) · 12.3 KB

Slashing Mechanism

Overview

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.

Core Concept

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

Design Philosophy

  1. Monotonic: Slashing only increases, never decreases (unless unslashing by admin)
  2. Fair: Prevents over-slashing — slash requests above the remaining available balance (bonded - slashed) are rejected
  3. Transparent: Events emit for all slashing operations; every slash is recorded in persistent history
  4. Accountable: Only authorized governance can execute

Authorization and Access Control

Admin-Only Execution

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

Security Properties

  • ✅ 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

Slashing Operations

slash_bond(admin, amount) → IdentityBond

Core slashing function.

Behavior:

  1. Validates caller is the contract admin (panics if not)
  2. Computes available balance = bonded_amount - slashed_amount
  3. Rejects zero or negative slash amounts
  4. Rejects slash requests above available balance
  5. Updates bond state with new slashed_amount using checked arithmetic
  6. Appends a normalized SlashRecord to persistent slash history
  7. Emits bond_slashed event
  8. Returns updated IdentityBond struct

Arguments:

  • admin: Address - Caller claiming admin authority
  • amount: i128 - Positive amount to slash

Returns:

  • IdentityBond with updated slashed_amount

Panics:

  • "not admin" if caller is not the contract admin
  • "no bond" if no bond exists
  • "slash amount must be positive" if amount <= 0
  • "slash exceeds bond" if the request would make slashed_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 == 700

Partial vs. Full Slashing

Partial 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

Over-Slash Prevention

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.

Slash History

Every successful call to slash_bond() appends a normalized SlashRecord to persistent storage, keyed by identity address and index.

SlashRecord Schema

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
}

Query Functions

// 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) -> SlashRecord

Notes

  • slash_amount in the record is the validated amount applied to the bond.
  • Records are stored in persistent storage and survive ledger TTL extensions.
  • Zero and negative slash amounts are rejected and do not append records.

State Management

Bond Structure

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
}

Withdrawals and Slashing

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

Event Emission

bond_slashed Event

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

Example Event Sequence

// 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"

Security Considerations

1. Authorization Bypass Prevention

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

2. Arithmetic Safety

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");
}

3. State Mutation Safety

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

4. Withdrawal Integration

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

Test Coverage

Test Categories (57 tests, 95%+ coverage)

  1. Basic Operations (4 tests)

    • Successful slash execution
    • Small amount slashing
    • Exact half slashing
    • Full amount slashing
  2. Authorization (3 tests)

    • Unauthorized rejection
    • Multiple unauthorized attempts
    • Identity cannot slash own bond
  3. Over-Slash Prevention (3 tests)

    • Amount exceeds bonded
    • Way over amount
    • Max i128 value rejection
  4. Edge Cases (3 tests)

    • Zero amount rejection
    • Negative amount rejection
    • Overflow prevention
    • Very large bonds (i128::MAX / 2)
  5. State Consistency (5 tests)

    • Single slash recording
    • Cumulative slashing
    • Multiple accumulation
    • Other fields unchanged
    • State persistence
  6. Event Emission (3 tests)

    • Event emitted on basic slash
    • Correct event data
    • Multiple events
  7. Withdrawal Integration (5 tests)

    • Withdraw respects available balance
    • Over-withdrawal prevention
    • Fully slashed bonds cannot withdraw
    • Exact available balance withdrawal
    • Complex slash/withdraw sequences
  8. Cumulative Scenarios (5 tests)

    • Cumulative over-cap rejection
    • Incremental slashing
    • Full slash prevents further slashing
    • Large amount accumulation
  9. State Persistence (2 tests)

    • State persists across calls
    • Slash result matches get_state
  10. Error Messages (2 tests)

    • "not admin" error
    • "no bond" error
  11. 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
  12. 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

Usage Examples

Example 1: Simple Penalty

// 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

Example 2: Escalating Penalties

// 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)

Example 3: Full Bond Forfeiture

// 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 withdraw

Example 4: Slashing and Withdrawal Sequence

let 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"

Comparison with Other Mechanisms

vs. Early Exit Penalty

  • Early Exit: Charged to users, transferred to treasury, applies at withdrawal time
  • Slashing: Imposed by governance, tracked in bond state, affects available balance

vs. Bond Top-Up

  • Top-Up: Increases bonded_amount (additive)
  • Slashing: Increases slashed_amount (subtractive, can't be reversed without unslashing)

vs. Bond Withdrawal

  • Withdrawal: Reduces bonded_amount (removes funds)
  • Slashing: Increases slashed_amount (blocks funds without removing)

Future Enhancements

  1. Partial Unslashing: Allow admin to reduce slashed_amount for appeals
  2. Treasury Integration: Actual fund transfers to governance treasury
  3. Slashing Tiers: Different slash amounts based on violation severity
  4. Timelocks: Delay slash execution for governance safety
  5. Signaling: Allow other addresses to propose slashing for governance review

References

Known Simplifications

Slashed funds are not transferred to the treasury in this reference implementation. See known-simplifications.md for details and the production path.