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
84 changes: 76 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ chance-staking/

```bash
cd chance-staking
cargo test # all 51 tests
cargo test # all 96 tests
cargo test -p chance-drand-oracle # oracle unit tests
cargo test -p chance-staking-hub # staking hub unit tests
cargo test -p chance-reward-distributor # distributor unit tests
Expand Down Expand Up @@ -66,6 +66,9 @@ Stores and verifies drand quicknet beacons (BLS signatures). Used by reward-dist

// Update operator list (admin only)
{ "update_operators": { "add": ["inj1..."], "remove": [] } }

// Update admin (admin only)
{ "update_admin": { "new_admin": "inj1..." } }
```

#### Query
Expand Down Expand Up @@ -117,6 +120,7 @@ Manages INJ staking, csINJ (liquid staking token) minting/burning, epoch advance
```jsonc
// Stake INJ to receive csINJ (send INJ in funds)
// csINJ_minted = inj_amount / exchange_rate
// Rejects if amount < min_stake_amount. Resets user's epoch eligibility timer.
{ "stake": {} }
// funds: [{ "denom": "inj", "amount": "1000000000000000000" }]

Expand All @@ -133,8 +137,9 @@ Manages INJ staking, csINJ (liquid staking token) minting/burning, epoch advance
{ "claim_rewards": {} }

// Step 2: Distribute claimed rewards and advance epoch (operator only)
// Enforces epoch_duration_seconds has elapsed since epoch start.
// Reads contract INJ balance, subtracts reserved unstake amounts,
// splits surplus: base_yield_bps -> backing, protocol_fee_bps -> treasury,
// splits surplus: base_yield_bps -> delegated to validators, protocol_fee_bps -> treasury,
// regular_pool_bps -> regular pool, big_pool_bps -> big pool
{ "distribute_rewards": {} }

Expand All @@ -148,15 +153,27 @@ Manages INJ staking, csINJ (liquid staking token) minting/burning, epoch advance
} }

// Update config (admin only)
// Note: BPS fields must sum to 10000 (regular + big + base_yield + protocol_fee)
{ "update_config": {
"admin": "inj1...", // optional
"operator": "inj1...", // optional
"protocol_fee_bps": 500 // optional
"protocol_fee_bps": 500, // optional
"base_yield_bps": 500, // optional
"regular_pool_bps": 7000, // optional
"big_pool_bps": 2000, // optional
"min_epochs_regular": 1, // optional, min epochs staked for regular draw eligibility
"min_epochs_big": 4, // optional, min epochs staked for big draw eligibility
"min_stake_amount": "1000" // optional, Uint128 (0 = no minimum)
} }

// Update validator set (admin only)
// Removed validators are automatically redelegated to remaining validators
// Validator addresses must start with "injvaloper"
{ "update_validators": { "add": ["injvaloper1..."], "remove": [] } }

// Sync backing with actual validator delegations after slashing (operator only)
// Updates TOTAL_INJ_BACKING, EPOCH_STATE.total_staked, and exchange rate
{ "sync_delegations": {} }
```

#### Query
Expand All @@ -177,7 +194,10 @@ Manages INJ staking, csINJ (liquid staking token) minting/burning, epoch advance
// "treasury": "inj1...",
// "base_yield_bps": 500,
// "regular_pool_bps": 7000,
// "big_pool_bps": 2000
// "big_pool_bps": 2000,
// "min_epochs_regular": 1,
// "min_epochs_big": 4,
// "min_stake_amount": "1000"
// }

// Get current epoch state
Expand Down Expand Up @@ -215,6 +235,14 @@ Manages INJ staking, csINJ (liquid staking token) minting/burning, epoch advance
// "claimed": false
// }
// }]

// Get staker eligibility info
{ "staker_info": { "address": "inj1..." } }
// Returns: StakerInfoResponse
// {
// "address": "inj1...",
// "stake_epoch": 5 | null // epoch of most recent stake, null if never staked
// }
```

---
Expand Down Expand Up @@ -273,10 +301,11 @@ Manages prize draw lifecycle: commit-reveal with drand randomness, merkle-proof
{ "expire_draw": { "draw_id": 0 } }

// Update config (admin only)
// reveal_deadline_seconds must be between 300 (5 min) and 86400 (24 hours)
{ "update_config": {
"operator": "inj1...", // optional
"staking_hub": "inj1...", // optional
"reveal_deadline_seconds": 3600, // optional
"reveal_deadline_seconds": 3600, // optional (300-86400)
"epochs_between_regular": 1, // optional
"epochs_between_big": 7 // optional
} }
Expand Down Expand Up @@ -399,13 +428,15 @@ interface SnapshotEntry {

## Merkle Tree

The merkle tree uses **sorted-pair hashing** (smaller hash first when combining siblings).
The merkle tree uses **sorted-pair hashing** (smaller hash first when combining siblings) with **domain separation** prefixes to prevent second pre-image attacks.

**Leaf hash**: `sha256(address_bytes || cumulative_start_be_u128 || cumulative_end_be_u128)`
**Leaf hash**: `sha256(0x00 || address_bytes || cumulative_start_be_u128 || cumulative_end_be_u128)`
- `0x00`: leaf domain separator prefix byte
- `address_bytes`: raw UTF-8 bytes of the bech32 address string
- `cumulative_start` / `cumulative_end`: big-endian 16-byte u128

**Internal nodes**: `sha256(min(left, right) || max(left, right))`
**Internal nodes**: `sha256(0x01 || min(left, right) || max(left, right))`
- `0x01`: internal node domain separator prefix byte

The frontend needs to:
1. Build the tree from the snapshot entries
Expand All @@ -423,6 +454,7 @@ The frontend needs to:
| Recent draws | reward-distributor | `draw_history` |
| Specific draw result | reward-distributor | `draw` |
| User's win history | reward-distributor | `user_wins` / `user_win_details` |
| Staker eligibility info | staking-hub | `staker_info` |
| Latest drand round | drand-oracle | `latest_round` |

## Key Frontend Actions
Expand All @@ -435,6 +467,25 @@ The frontend needs to:

---

## Validation Rules (Post-Audit)

- **BPS sum**: `regular_pool_bps + big_pool_bps + base_yield_bps + protocol_fee_bps` must equal 10000. Enforced at instantiation and `update_config`.
- **Validator addresses**: Must start with `injvaloper` and be reasonable length. Enforced at instantiation and `update_validators`.
- **Merkle root**: Must be exactly 64 hex characters (32 bytes). Validated in `take_snapshot`.
- **Reveal deadline**: Must be between 300 seconds (5 min) and 86400 seconds (24 hours). Enforced at instantiation and `update_config`.
- **Epoch duration**: `distribute_rewards` enforces that `epoch_duration_seconds` has elapsed since epoch start.
- **Snapshot overwrite**: Cannot overwrite a snapshot for an epoch that already has one.
- **Zero weight**: Snapshots with `total_weight = 0` are rejected at `commit_draw`.
- **Balance check**: `reveal_draw` verifies contract has sufficient balance before payout.
- **Min stake**: Stake amount must be >= `min_stake_amount` (configurable, 0 = no minimum).
- **Draw epoch**: `commit_draw` validates the epoch matches the latest snapshot epoch.

## Contract Migration

All three contracts support `migrate()` via `MigrateMsg {}` (empty message). Uses cw2 for contract name/version validation.

---

## Known Design Trade-offs

### Draw Reveal Discretion (L-02)
Expand All @@ -460,3 +511,20 @@ donates to all current stakers and is not a security risk.
- Direct transfers are treated as additional staking rewards
- The INJ is split according to BPS configuration (pools, treasury, base yield)
- This is by design and allows for voluntary contributions to the reward pool

### Re-staking Resets Eligibility (V2-I-01)

Any new stake resets the user's epoch eligibility timer (`USER_STAKE_EPOCH`).
This means adding more INJ restarts the `min_epochs_regular` / `min_epochs_big`
countdown for draw eligibility.

**Implications:**

- Frontend should warn users that additional stakes reset their eligibility timer
- Users who want to remain eligible should avoid staking more until after a draw

### No Minimum Stake Enforced by Default (V2-L-03)

The `min_stake_amount` config defaults to 0 (no minimum). Dust stakes are allowed
since winning probability is proportional to stake weight, making the expected
value for tiny stakes negligible. Operators can set a minimum via `update_config`.
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Optimized `.wasm` files output to `chance-staking/artifacts/`.

```bash
cd chance-staking
cargo test # all tests
cargo test # all 96 tests
cargo test -p chance-drand-oracle # oracle unit tests
cargo test -p chance-staking-hub # staking hub unit tests
cargo test -p chance-reward-distributor # distributor unit tests
Expand Down Expand Up @@ -239,10 +239,23 @@ Chain: `injective-888` (Injective Testnet)
## Key Concepts

- **csINJ**: Liquid staking token minted via Token Factory. Exchange rate starts at 1.0 and increases as base yield accrues: `rate = total_inj_backing / total_csinj_supply`
- **Epochs**: Time periods (configurable, default 24h) after which rewards are harvested and distributed
- **Merkle Tree**: Sorted-pair hashing (`sha256(min(left,right) || max(left,right))`) for snapshot verification. Leaf: `sha256(address_bytes || cumulative_start_be_u128 || cumulative_end_be_u128)`
- **Epochs**: Time periods (configurable, default 24h) after which rewards are harvested and distributed. Epoch duration is enforced on-chain. Users must be staked for `min_epochs_regular` / `min_epochs_big` epochs to be eligible for draws
- **Merkle Tree**: Sorted-pair hashing with domain separation. Leaf: `sha256(0x00 || address_bytes || cumulative_start_be_u128 || cumulative_end_be_u128)`. Internal: `sha256(0x01 || min(left,right) || max(left,right))`
- **Commit-Reveal**: Two-phase draw to prevent manipulation. Operator commits before randomness is known, reveals after the drand beacon is available
- **Unstaking**: 21-day unbonding period (Injective native). Users call `unstake` then `claim_unstaked` after the lock expires
- **Minimum Stake**: Configurable `min_stake_amount` per transaction (default 0 = no minimum). Re-staking resets the user's epoch eligibility timer
- **Contract Migration**: All three contracts support on-chain migration via `MigrateMsg {}`

## Security Audits

Two security audits have been completed with all 24 findings remediated:

- **Audit V1**: 17 findings (2 critical, 5 high, 5 medium, 5 low) — all fixed
- **Audit V2**: 7 findings (3 medium, 3 low, 1 informational) — all fixed

Key security improvements include: BPS sum validation, epoch duration enforcement, merkle domain separation, validator address validation, reveal deadline bounds, slashing detection (`sync_delegations`), snapshot overwrite prevention, and contract migration support.

Full reports and fix tracking are in [`chance-staking/docs/`](chance-staking/docs/).

## License

Expand Down
8 changes: 4 additions & 4 deletions chance-staking/scripts/deploy_testnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ QUICKNET_GENESIS_TIME=1692803367
QUICKNET_PERIOD_SECONDS=3

# -- Reward distributor config --
REVEAL_DEADLINE_SECONDS=180
REVEAL_DEADLINE_SECONDS=300
EPOCHS_BETWEEN_REGULAR=1
EPOCHS_BETWEEN_BIG=6

Expand All @@ -66,9 +66,9 @@ STAKING_HUB_WASM="./artifacts/chance_staking_hub.wasm"
# -- Optional: reuse existing code IDs (set to skip uploading) --
# If set, the script will skip storing that contract's wasm and use the given ID.
# Leave empty to upload fresh.
EXISTING_DRAND_CODE_ID=""
EXISTING_DISTRIBUTOR_CODE_ID=""
EXISTING_STAKING_HUB_CODE_ID=""
EXISTING_DRAND_CODE_ID="39250"
EXISTING_DISTRIBUTOR_CODE_ID="39251"
EXISTING_STAKING_HUB_CODE_ID="39252"

################################################################################
# HELPERS #
Expand Down
12 changes: 6 additions & 6 deletions deployed.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Testnet

drand-oracle code ID: 39244
drand-oracle code ID: 39250

drand-oracle address: inj12dg907vrnw3zdsh8hjvf4ywqky8gw7e3v7lwf7
drand-oracle address: inj1jwztm5q5gnaq0jgt36v8wkskx6ryyul9nx4q6a

reward-distributor code ID: 39245
reward-distributor code ID: 39251

reward-distributor address: inj1thz9kqf74w4a8yakpx62xmnll3nf032rnnukyy
reward-distributor address: inj1pzl6p4el05lum6qd3h2e78gfsnaztll8g54fmr

staking-hub code ID: 39246
staking-hub code ID: 39252

staking-hub address: inj15vq83p8l6wl7qneulzgnt66dwheh2ecpprj0kn
staking-hub address: inj17l2r0vgfuv4sl6j2m47fhl8fypa6jezne5hdav
Loading