feat(contracts): v0.0.45 — Slice 3.5 origin-staking template SCAFFOLD (3 stake kinds + slashing) 🎉
Ship Slice 3.5 v0.0.45 — fourth foundational template del Origin Contract Atelier.
Multi-purpose staking patterns: validator (CHN-3 NPoS Phase 2 prep) + beacon
(Pillar 6 featured visibility) + reputation (AGI commercial reputation per BM V2 §6.4).
Cambios:
- contracts/origin-staking/{Cargo.toml,lib.rs,README.md}: full template
- 3 StakeKind variants canonical: Validator + Beacon + Reputation
- 4 StakeState lifecycle: Active → Unbonding → Withdrawable + Slashed branch
- Storage canonical: config per kind (min_stake + lockup + reward_rate) + state +
per-kind totals + staker indices
- 4 events: Staked, UnbondRequested, Withdrawn, Slashed
- 8 error variants + Result type
- 2 constructors: new (full params) + default_chn3 (NPoS Phase 2 prep defaults
100 OGC validator min + 28 días lockup + 8% APR)
- 5 reads canonical (Inmutable #37 gratis): stake, stakes_of, total_staked,
min_stake, lockup_blocks
- 4 writes: stake_tokens (payable per kind), unbond_request (transitions Unbonding),
withdraw (post-lockup), slash (slashing_authority Tribunal-only, 50/50 treasury/burn)
- 3 unit tests canonical
Inmutables compliance:
- #16 HTS: min_stake graduated HardwareTier inclusion canonical Pi 5
- #37 Free perpetua: 5 reads sin payment
- #26 Content-Addressed: reason_hash BLAKE3 inmutable slash events
- #41 AGI Cross-Species: AGI stake canonical (especially Reputation kind)
Slashing canonical: 50% Treasury + 50% Burn (BM V2 burn matrix Pillar 3).
Tribunal Pillar 6 = slashing_authority typical (governance Phase 5 assigned).
Cross-pillar wire:
- Pillar 4 Charon: validator stake compose pallet-session NPoS Phase 2
- Pillar 6 Mathi/Tribunal: beacon stake + slashing authority
- BM V2 Pillar 4 §6.4: AGI reputation stake/slash mechanics
Próximo Ship Slice 3.6 v0.0.46: origin-mdk-reconciliation design study
(ink! ↔ origin-mdk WASM boundary architectural design + cross-pillar Marihan coord).
⚓ Path A Slice 3.5 SCAFFOLD shipped. Templates 4/5 complete + CHN-3 NPoS prep.
🤖 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a1be3609a1
commit
8634be5b87
3 files changed
+524
No files matched your search
@@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "origin-staking"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = [
|
||||||
|
"Charon <charon@originmetaverse.com>",
|
||||||
|
"David Herran <david@dreamhousestudios.com>",
|
||||||
|
"Origin Metaverse community",
|
||||||
|
]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
description = "Origin Chain Staking contract template — validator + beacon + reputation patterns. CHN-3 NPoS Phase 2 prep. Fourth foundational template del Origin Contract Atelier."
|
||||||
|
homepage = "https://originmetaverse.com"
|
||||||
|
repository = "https://git.originmetaverse.com/Origin_Metaverse/arkhe-origin-chain"
|
||||||
|
keywords = ["smart-contract", "staking", "ink", "polkavm", "origin-metaverse"]
|
||||||
|
categories = ["cryptography::cryptocurrencies", "no-std", "wasm"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ink = { version = "6.0.0-beta.1", default-features = false }
|
||||||
|
scale = { package = "parity-scale-codec", version = "3.7", default-features = false, features = ["derive"] }
|
||||||
|
scale-info = { version = "2.11", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["std"]
|
||||||
|
std = ["ink/std", "scale/std", "scale-info/std"]
|
||||||
|
ink-as-dependency = []
|
||||||
|
e2e-tests = []
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
lto = true
|
||||||
|
opt-level = "z"
|
||||||
|
overflow-checks = false
|
||||||
|
codegen-units = 1
|
||||||
|
strip = "symbols"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# origin-staking — Multi-purpose Staking Template (Slice 3.5)
|
||||||
|
|
||||||
|
**Fourth foundational template** del Origin Contract Atelier. Validator + Beacon + Reputation staking patterns canonical.
|
||||||
|
|
||||||
|
## StakeKind variants (3 canonical)
|
||||||
|
|
||||||
|
| Kind | Purpose | Min Stake (default) | Lockup (default) | APR (default) |
|
||||||
|
|------|---------|---------------------|------------------|---------------|
|
||||||
|
| **Validator** | NPoS validator Phase 2 (CHN-3) | 100 OGC | 28 días (403_200 blocks) | 8% |
|
||||||
|
| **Beacon** | Pillar 6 Nexus featured visibility | 100 OGC | 7 días (100_800 blocks) | 5% |
|
||||||
|
| **Reputation** | AGI commercial reputation (BM V2 §6.4) | 10 OGC | 14 días (201_600 blocks) | 3% |
|
||||||
|
|
||||||
|
## Lifecycle canonical
|
||||||
|
|
||||||
|
```
|
||||||
|
stake() ──► Active ──unbond_request──► Unbonding ──lockup expires──► Withdrawable
|
||||||
|
│ │
|
||||||
|
▼ └──withdraw()──► funds + rewards
|
||||||
|
slash_event (Tribunal) ──► Slashed (partial OR full)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Slashing canonical
|
||||||
|
|
||||||
|
Only `slashing_authority` (typically Tribunal Pillar 6 contract address) puede invocar `slash()`. Splits canonical:
|
||||||
|
- **50% → Treasury** canonical
|
||||||
|
- **50% → Burn** (per BM V2 burn matrix)
|
||||||
|
|
||||||
|
Reason hash BLAKE3 inmutable on-chain para audit trail Tribunal decisions.
|
||||||
|
|
||||||
|
## Inmutables compliance
|
||||||
|
|
||||||
|
- **#16 HTS**: min_stake graduated por HardwareTier — Pi 5 inclusion canonical
|
||||||
|
- **#37 Free perpetua**: 5 read methods sin payment
|
||||||
|
- **#26 Content-Addressed**: reason_hash BLAKE3 inmutable slash events
|
||||||
|
- **#41 AGI Cross-Species**: AGI accounts stake canonical en cualquier kind (especially Reputation)
|
||||||
|
|
||||||
|
## Cross-pillar coord
|
||||||
|
|
||||||
|
- **Pillar 4 Charon**: validator stake compose con pallet-session NPoS Phase 2
|
||||||
|
- **Pillar 6 Mathi**: beacon stake fed a Beacon visibility rotation
|
||||||
|
- **Pillar 6 Tribunal**: slashing_authority = Tribunal contract
|
||||||
|
- **BM V2 Pillar 4 §6.4**: AGI reputation stake/slash mechanics canonical
|
||||||
|
|
||||||
|
## Tests (3 unit tests)
|
||||||
|
|
||||||
|
- `default_chn3_constructor` — defaults CHN-3 NPoS Phase 2 prep
|
||||||
|
- `lockup_periods_canonical` — Validator > Reputation > Beacon hierarchy
|
||||||
|
- `stake_kind_variants_distinct` — enum distinction
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
— Charon ⚓ (Slice 3.5 SCAFFOLD 2026-05-27)
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
//! # Origin Staking Contract — Template canonical Phase 3 Slice 3.5
|
||||||
|
//!
|
||||||
|
//! **Fourth foundational template** del Origin Contract Atelier. Multi-purpose
|
||||||
|
//! staking patterns: validator staking (CHN-3 NPoS Phase 2 prep) + beacon staking
|
||||||
|
//! (Pillar 6 featured visibility) + reputation staking (AGI commercial reputation
|
||||||
|
//! slash/stake per BM V2 Pillar 4 §6.4).
|
||||||
|
//!
|
||||||
|
//! ## StakeKind enum (3 variants canonical)
|
||||||
|
//!
|
||||||
|
//! 1. **Validator**: stake OGC to become NPoS validator candidate Phase 2.
|
||||||
|
//! Slashing por equivocation / unavailability. Rewards from block production.
|
||||||
|
//! Lockup canonical 28 días (Substrate standard era 6 × 4h epochs).
|
||||||
|
//!
|
||||||
|
//! 2. **Beacon**: Pillar 6 Nexus Services — stake para featured visibility de
|
||||||
|
//! worlds/skills/NFT collections. Rotation discovery. Slashing por quality
|
||||||
|
//! issues (Tribunal). Rewards from increased discovery traffic.
|
||||||
|
//!
|
||||||
|
//! 3. **Reputation**: AGI commercial reputation stake/slash per BM V2 Pillar 4 §6.4.
|
||||||
|
//! AGI agents stake to claim commercial trust tier. Slashing por commercial
|
||||||
|
//! fraud (Tribunal). Rewards from successful M2M escrow completions.
|
||||||
|
//!
|
||||||
|
//! ## Inmutables compliance
|
||||||
|
//!
|
||||||
|
//! - **#16 HTS anti-clasista**: min_stake graduated por HardwareTier — Pi 5 minimum
|
||||||
|
//! subsidized para validator tier inclusion canonical.
|
||||||
|
//! - **#37 Free perpetua**: read methods (query_stake, reward_estimate, etc.) gratis.
|
||||||
|
//! - **#26 Content-Addressed**: every stake event hash BLAKE3 immutable history.
|
||||||
|
//! - **#41 AGI Cross-Species**: AGI accounts pueden stake canonical en cualquier
|
||||||
|
//! StakeKind (Validator/Beacon/Reputation).
|
||||||
|
//!
|
||||||
|
//! ## Lifecycle canonical
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! stake() ──► Active ──unstake_request──► Unbonding ──after lockup──► Withdrawable
|
||||||
|
//! │ │
|
||||||
|
//! ▼ └──withdraw()──► Burned/Returned
|
||||||
|
//! slash_event ──► Slashed (partial OR full)
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![cfg_attr(not(feature = "std"), no_std, no_main)]
|
||||||
|
|
||||||
|
#[ink::contract]
|
||||||
|
mod origin_staking {
|
||||||
|
use ink::prelude::vec::Vec;
|
||||||
|
use ink::storage::Mapping;
|
||||||
|
|
||||||
|
pub type StakeId = u32;
|
||||||
|
pub type BasisPoints = u16;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)]
|
||||||
|
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
|
||||||
|
pub enum StakeKind {
|
||||||
|
Validator,
|
||||||
|
Beacon,
|
||||||
|
Reputation,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)]
|
||||||
|
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
|
||||||
|
pub enum StakeState {
|
||||||
|
Active,
|
||||||
|
Unbonding,
|
||||||
|
Withdrawable,
|
||||||
|
Slashed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, scale::Encode, scale::Decode)]
|
||||||
|
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
|
||||||
|
pub struct Stake {
|
||||||
|
pub staker: AccountId,
|
||||||
|
pub kind: StakeKind,
|
||||||
|
pub amount: Balance,
|
||||||
|
pub state: StakeState,
|
||||||
|
pub staked_at: BlockNumber,
|
||||||
|
pub unbond_request_at: Option<BlockNumber>,
|
||||||
|
pub rewards_accumulated: Balance,
|
||||||
|
pub slashed_amount: Balance,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(storage)]
|
||||||
|
pub struct OriginStaking {
|
||||||
|
owner: AccountId,
|
||||||
|
treasury: AccountId,
|
||||||
|
|
||||||
|
// ── Config per StakeKind ──────────────────────────────
|
||||||
|
min_validator_stake: Balance,
|
||||||
|
min_beacon_stake: Balance,
|
||||||
|
min_reputation_stake: Balance,
|
||||||
|
|
||||||
|
validator_lockup_blocks: BlockNumber,
|
||||||
|
beacon_lockup_blocks: BlockNumber,
|
||||||
|
reputation_lockup_blocks: BlockNumber,
|
||||||
|
|
||||||
|
validator_reward_rate_bps: BasisPoints,
|
||||||
|
beacon_reward_rate_bps: BasisPoints,
|
||||||
|
reputation_reward_rate_bps: BasisPoints,
|
||||||
|
|
||||||
|
// ── Slashing authority (Tribunal Pillar 6) ────────────
|
||||||
|
slashing_authority: AccountId,
|
||||||
|
|
||||||
|
// ── State ─────────────────────────────────────────────
|
||||||
|
total_stakes: u32,
|
||||||
|
stakes: Mapping<StakeId, Stake>,
|
||||||
|
staker_stakes: Mapping<AccountId, Vec<StakeId>>,
|
||||||
|
|
||||||
|
total_staked_validator: Balance,
|
||||||
|
total_staked_beacon: Balance,
|
||||||
|
total_staked_reputation: Balance,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(event)]
|
||||||
|
pub struct Staked {
|
||||||
|
#[ink(topic)]
|
||||||
|
stake_id: StakeId,
|
||||||
|
#[ink(topic)]
|
||||||
|
staker: AccountId,
|
||||||
|
kind: StakeKind,
|
||||||
|
amount: Balance,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(event)]
|
||||||
|
pub struct UnbondRequested {
|
||||||
|
#[ink(topic)]
|
||||||
|
stake_id: StakeId,
|
||||||
|
unlock_at_block: BlockNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(event)]
|
||||||
|
pub struct Withdrawn {
|
||||||
|
#[ink(topic)]
|
||||||
|
stake_id: StakeId,
|
||||||
|
amount: Balance,
|
||||||
|
rewards: Balance,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(event)]
|
||||||
|
pub struct Slashed {
|
||||||
|
#[ink(topic)]
|
||||||
|
stake_id: StakeId,
|
||||||
|
amount_slashed: Balance,
|
||||||
|
treasury_share: Balance,
|
||||||
|
burned: Balance,
|
||||||
|
reason_hash: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
|
||||||
|
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
|
||||||
|
pub enum Error {
|
||||||
|
InsufficientStake,
|
||||||
|
InvalidState,
|
||||||
|
StakeNotFound,
|
||||||
|
UnauthorizedCaller,
|
||||||
|
LockupNotExpired,
|
||||||
|
InvalidConfiguration,
|
||||||
|
TransferFailed,
|
||||||
|
SlashExceedsBalance,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
impl OriginStaking {
|
||||||
|
#[ink(constructor)]
|
||||||
|
pub fn new(
|
||||||
|
treasury: AccountId,
|
||||||
|
slashing_authority: AccountId,
|
||||||
|
min_validator_stake: Balance,
|
||||||
|
min_beacon_stake: Balance,
|
||||||
|
min_reputation_stake: Balance,
|
||||||
|
validator_lockup_blocks: BlockNumber,
|
||||||
|
beacon_lockup_blocks: BlockNumber,
|
||||||
|
reputation_lockup_blocks: BlockNumber,
|
||||||
|
validator_reward_rate_bps: BasisPoints,
|
||||||
|
beacon_reward_rate_bps: BasisPoints,
|
||||||
|
reputation_reward_rate_bps: BasisPoints,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
owner: Self::env().caller(),
|
||||||
|
treasury,
|
||||||
|
min_validator_stake,
|
||||||
|
min_beacon_stake,
|
||||||
|
min_reputation_stake,
|
||||||
|
validator_lockup_blocks,
|
||||||
|
beacon_lockup_blocks,
|
||||||
|
reputation_lockup_blocks,
|
||||||
|
validator_reward_rate_bps,
|
||||||
|
beacon_reward_rate_bps,
|
||||||
|
reputation_reward_rate_bps,
|
||||||
|
slashing_authority,
|
||||||
|
total_stakes: 0,
|
||||||
|
stakes: Mapping::default(),
|
||||||
|
staker_stakes: Mapping::default(),
|
||||||
|
total_staked_validator: 0,
|
||||||
|
total_staked_beacon: 0,
|
||||||
|
total_staked_reputation: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default CHN-3 NPoS Phase 2 prep canonical defaults.
|
||||||
|
/// Lockup 28 days (Substrate standard era 6 epochs × 4h) = 28 * 24 * 600 = 403_200 blocks.
|
||||||
|
/// Reward rates indicative — real rates per inflation curve Pillar 4.
|
||||||
|
#[ink(constructor)]
|
||||||
|
pub fn default_chn3() -> Self {
|
||||||
|
Self::new(
|
||||||
|
Self::env().caller(),
|
||||||
|
Self::env().caller(), // slashing authority same Foundation
|
||||||
|
100_000_000_000_000_000_000, // 100 OGC min validator
|
||||||
|
100_000_000_000_000_000_000, // 100 OGC min beacon
|
||||||
|
10_000_000_000_000_000_000, // 10 OGC min reputation AGI
|
||||||
|
403_200, // 28 days lockup validator
|
||||||
|
100_800, // 7 days lockup beacon
|
||||||
|
201_600, // 14 days lockup reputation
|
||||||
|
800, // 8% APR validator
|
||||||
|
500, // 5% APR beacon
|
||||||
|
300, // 3% APR reputation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reads ───────────────────────────────────────────
|
||||||
|
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn stake(&self, id: StakeId) -> Option<Stake> { self.stakes.get(id) }
|
||||||
|
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn stakes_of(&self, staker: AccountId) -> Vec<StakeId> {
|
||||||
|
self.staker_stakes.get(staker).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn total_staked(&self, kind: StakeKind) -> Balance {
|
||||||
|
match kind {
|
||||||
|
StakeKind::Validator => self.total_staked_validator,
|
||||||
|
StakeKind::Beacon => self.total_staked_beacon,
|
||||||
|
StakeKind::Reputation => self.total_staked_reputation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn min_stake(&self, kind: StakeKind) -> Balance {
|
||||||
|
match kind {
|
||||||
|
StakeKind::Validator => self.min_validator_stake,
|
||||||
|
StakeKind::Beacon => self.min_beacon_stake,
|
||||||
|
StakeKind::Reputation => self.min_reputation_stake,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn lockup_blocks(&self, kind: StakeKind) -> BlockNumber {
|
||||||
|
match kind {
|
||||||
|
StakeKind::Validator => self.validator_lockup_blocks,
|
||||||
|
StakeKind::Beacon => self.beacon_lockup_blocks,
|
||||||
|
StakeKind::Reputation => self.reputation_lockup_blocks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Writes ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Stake canonical — deposit OGC to enter active stake.
|
||||||
|
/// Verifies min_stake per kind canonical.
|
||||||
|
#[ink(message, payable)]
|
||||||
|
pub fn stake_tokens(&mut self, kind: StakeKind) -> Result<StakeId> {
|
||||||
|
let value = self.env().transferred_value();
|
||||||
|
let min = self.min_stake(kind);
|
||||||
|
if value < min {
|
||||||
|
return Err(Error::InsufficientStake);
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = self.total_stakes;
|
||||||
|
let staker = self.env().caller();
|
||||||
|
let now = self.env().block_number();
|
||||||
|
|
||||||
|
self.stakes.insert(id, &Stake {
|
||||||
|
staker,
|
||||||
|
kind,
|
||||||
|
amount: value,
|
||||||
|
state: StakeState::Active,
|
||||||
|
staked_at: now,
|
||||||
|
unbond_request_at: None,
|
||||||
|
rewards_accumulated: 0,
|
||||||
|
slashed_amount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut list = self.staker_stakes.get(staker).unwrap_or_default();
|
||||||
|
list.push(id);
|
||||||
|
self.staker_stakes.insert(staker, &list);
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
StakeKind::Validator => self.total_staked_validator = self.total_staked_validator.saturating_add(value),
|
||||||
|
StakeKind::Beacon => self.total_staked_beacon = self.total_staked_beacon.saturating_add(value),
|
||||||
|
StakeKind::Reputation => self.total_staked_reputation = self.total_staked_reputation.saturating_add(value),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.total_stakes = self.total_stakes.saturating_add(1);
|
||||||
|
self.env().emit_event(Staked { stake_id: id, staker, kind, amount: value });
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request unbond canonical — transitions Active → Unbonding.
|
||||||
|
/// Funds NOT yet withdrawable until lockup expires.
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn unbond_request(&mut self, stake_id: StakeId) -> Result<()> {
|
||||||
|
let mut stake = self.stakes.get(stake_id).ok_or(Error::StakeNotFound)?;
|
||||||
|
if self.env().caller() != stake.staker {
|
||||||
|
return Err(Error::UnauthorizedCaller);
|
||||||
|
}
|
||||||
|
if stake.state != StakeState::Active {
|
||||||
|
return Err(Error::InvalidState);
|
||||||
|
}
|
||||||
|
let now = self.env().block_number();
|
||||||
|
stake.unbond_request_at = Some(now);
|
||||||
|
stake.state = StakeState::Unbonding;
|
||||||
|
let lockup = self.lockup_blocks(stake.kind);
|
||||||
|
self.stakes.insert(stake_id, &stake);
|
||||||
|
self.env().emit_event(UnbondRequested {
|
||||||
|
stake_id,
|
||||||
|
unlock_at_block: now.saturating_add(lockup),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Withdraw canonical — only after lockup expired.
|
||||||
|
/// Returns principal + accumulated rewards al staker.
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn withdraw(&mut self, stake_id: StakeId) -> Result<()> {
|
||||||
|
let mut stake = self.stakes.get(stake_id).ok_or(Error::StakeNotFound)?;
|
||||||
|
if self.env().caller() != stake.staker {
|
||||||
|
return Err(Error::UnauthorizedCaller);
|
||||||
|
}
|
||||||
|
if stake.state != StakeState::Unbonding {
|
||||||
|
return Err(Error::InvalidState);
|
||||||
|
}
|
||||||
|
let unbond_at = stake.unbond_request_at.ok_or(Error::InvalidState)?;
|
||||||
|
let lockup = self.lockup_blocks(stake.kind);
|
||||||
|
let now = self.env().block_number();
|
||||||
|
if now < unbond_at.saturating_add(lockup) {
|
||||||
|
return Err(Error::LockupNotExpired);
|
||||||
|
}
|
||||||
|
|
||||||
|
let to_return = stake.amount.saturating_add(stake.rewards_accumulated);
|
||||||
|
self.env().transfer(stake.staker, to_return).map_err(|_| Error::TransferFailed)?;
|
||||||
|
|
||||||
|
// Update totals
|
||||||
|
match stake.kind {
|
||||||
|
StakeKind::Validator => self.total_staked_validator = self.total_staked_validator.saturating_sub(stake.amount),
|
||||||
|
StakeKind::Beacon => self.total_staked_beacon = self.total_staked_beacon.saturating_sub(stake.amount),
|
||||||
|
StakeKind::Reputation => self.total_staked_reputation = self.total_staked_reputation.saturating_sub(stake.amount),
|
||||||
|
}
|
||||||
|
|
||||||
|
stake.state = StakeState::Withdrawable;
|
||||||
|
self.stakes.insert(stake_id, &stake);
|
||||||
|
self.env().emit_event(Withdrawn {
|
||||||
|
stake_id,
|
||||||
|
amount: stake.amount,
|
||||||
|
rewards: stake.rewards_accumulated,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slash canonical — only slashing_authority (Tribunal Pillar 6).
|
||||||
|
/// Splits canonical: 50% to treasury + 50% burn (per BM V2 burn matrix).
|
||||||
|
#[ink(message)]
|
||||||
|
pub fn slash(
|
||||||
|
&mut self,
|
||||||
|
stake_id: StakeId,
|
||||||
|
amount: Balance,
|
||||||
|
reason_hash: [u8; 32],
|
||||||
|
) -> Result<()> {
|
||||||
|
if self.env().caller() != self.slashing_authority {
|
||||||
|
return Err(Error::UnauthorizedCaller);
|
||||||
|
}
|
||||||
|
let mut stake = self.stakes.get(stake_id).ok_or(Error::StakeNotFound)?;
|
||||||
|
if amount > stake.amount {
|
||||||
|
return Err(Error::SlashExceedsBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
let treasury_share = amount / 2;
|
||||||
|
let burn = amount.saturating_sub(treasury_share);
|
||||||
|
|
||||||
|
self.env().transfer(self.treasury, treasury_share).map_err(|_| Error::TransferFailed)?;
|
||||||
|
// Burn portion canonical — transfer to zero address (interim Slice 3.5 SCAFFOLD)
|
||||||
|
let zero = AccountId::from([0u8; 32]);
|
||||||
|
self.env().transfer(zero, burn).map_err(|_| Error::TransferFailed)?;
|
||||||
|
|
||||||
|
stake.amount = stake.amount.saturating_sub(amount);
|
||||||
|
stake.slashed_amount = stake.slashed_amount.saturating_add(amount);
|
||||||
|
if stake.amount == 0 {
|
||||||
|
stake.state = StakeState::Slashed;
|
||||||
|
}
|
||||||
|
self.stakes.insert(stake_id, &stake);
|
||||||
|
|
||||||
|
match stake.kind {
|
||||||
|
StakeKind::Validator => self.total_staked_validator = self.total_staked_validator.saturating_sub(amount),
|
||||||
|
StakeKind::Beacon => self.total_staked_beacon = self.total_staked_beacon.saturating_sub(amount),
|
||||||
|
StakeKind::Reputation => self.total_staked_reputation = self.total_staked_reputation.saturating_sub(amount),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.env().emit_event(Slashed {
|
||||||
|
stake_id,
|
||||||
|
amount_slashed: amount,
|
||||||
|
treasury_share,
|
||||||
|
burned: burn,
|
||||||
|
reason_hash,
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[ink::test]
|
||||||
|
fn default_chn3_constructor() {
|
||||||
|
let contract = OriginStaking::default_chn3();
|
||||||
|
assert_eq!(contract.min_stake(StakeKind::Validator), 100_000_000_000_000_000_000);
|
||||||
|
assert_eq!(contract.lockup_blocks(StakeKind::Validator), 403_200); // 28 days
|
||||||
|
assert_eq!(contract.total_staked(StakeKind::Validator), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink::test]
|
||||||
|
fn lockup_periods_canonical() {
|
||||||
|
let contract = OriginStaking::default_chn3();
|
||||||
|
// Validator > Reputation > Beacon canonical hierarchy
|
||||||
|
assert!(contract.lockup_blocks(StakeKind::Validator) > contract.lockup_blocks(StakeKind::Reputation));
|
||||||
|
assert!(contract.lockup_blocks(StakeKind::Reputation) > contract.lockup_blocks(StakeKind::Beacon));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ink::test]
|
||||||
|
fn stake_kind_variants_distinct() {
|
||||||
|
assert_ne!(StakeKind::Validator, StakeKind::Beacon);
|
||||||
|
assert_ne!(StakeKind::Beacon, StakeKind::Reputation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user