帖子
分享您的知识。
Multi-Asset Staking Mechanisms
What strategies can I use to implement multi-asset staking in Sui where reward distribution dynamically adjusts without locking the underlying pools?
- Sui
- Architecture
- Transaction Processing
- Security Protocols
- NFT Ecosystem
答案
5I’ve built multi-asset staking systems before and the trick is to make rewards accounting flexible while keeping the asset pools free to move (no hard locks). Below I give a set of practical strategies I use (why, pros/cons), concrete Move patterns you can drop into Sui, and an operational/testing checklist. I write in first person and include multiple code snippets so you can pick the pattern(s) that fit your product.
High-level strategies
- Per-asset share tokens +
accRewardPerShare
(lazy accounting). Very gas-efficient; pools remain unlocked. - Boost multipliers (ve-token / tenure) applied at claim time. Dynamic weights without locking underlying LPs.
- Virtual balances / strategy adapters so underlying assets can be rebalanced while user shares remain stable.
- Off-chain accrual + Merkle claims for ultra-high-frequency micro-rewards. Publish roots on-chain, users prove entitlement.
- Reward policy objects (on-chain pluggable rules) to change distribution formulas without migrating state.
- Emergency redeem / queued withdrawals to let users extract when off-chain strategies are illiquid.
I typically combine (1) + (2) + (3) for a production staking vault: share tokens + lazy accruals + boosts + strategy adapters.
1) Per-asset shares + accRewardPerShare
(the core pattern)
Why I use it: avoid iterating over all LP positions on every reward update. I update an accumulator when rewards arrive; users claim lazily (compute owed = shares * delta(acc) - reward_debt).
Pros: very gas efficient for writes; claims are O(1). Cons: claim tx touches user share object + pool object; must avoid rounding errors.
Move sketch (core accounting)
module staking::simple {
use sui::object::{UID};
use sui::tx_context::TxContext;
use std::vector;
const PRECISION: u128 = 1_000_000_000u128;
struct Pool has key {
id: UID,
asset_type: vector<u8>, // token identifier
total_shares: u128,
acc_reward_per_share: u128, // scaled by PRECISION
}
// Each user's stake represented by an owned share object
struct Share has key {
id: UID,
owner: address,
shares: u128,
reward_debt: u128, // shares * acc_reward_per_share at deposit time
}
// Add reward to pool (called by strategy or distributor)
public(entry) fun deposit_rewards(pool: &mut Pool, reward: u128) {
assert!(pool.total_shares > 0, 1);
pool.acc_reward_per_share = pool.acc_reward_per_share + (reward * PRECISION / pool.total_shares);
}
// User deposits underlying and receives share object (minted)
public(entry) fun deposit(pool: &mut Pool, owner: address, amt_shares: u128, ctx: &mut TxContext): Share {
pool.total_shares = pool.total_shares + amt_shares;
let id = object::new(ctx);
let debt = pool.acc_reward_per_share * amt_shares / PRECISION;
Share { id, owner, shares: amt_shares, reward_debt: debt }
}
// Claim pending rewards
public(entry) fun claim(share: &mut Share, pool: &Pool) {
let pending = (pool.acc_reward_per_share * share.shares / PRECISION) - share.reward_debt;
// transfer pending to share.owner (implementation-specific)
// update reward_debt
share.reward_debt = pool.acc_reward_per_share * share.shares / PRECISION;
}
// Withdraw: claim + burn shares, decrease total_shares
public(entry) fun withdraw(share: Share, pool: &mut Pool) {
// claim logic then burn share (object deleted) and reduce pool.total_shares
}
}
Important details I always enforce
- Use fixed
PRECISION
to avoid fractional truncation issues. - Store
reward_debt
per-share at deposit/withdraw to compute owed correctly. - When you change
acc_reward_per_share
ensuretotal_shares > 0
, otherwise stash rewards to a buffer.
2) Dynamic boosts (ve-token, tenure, NFT boosts) — apply at claim time
I do not permanently alter shares. Instead I compute an effective share at claim time:
effective_shares = base_shares * (1 + boost_multiplier)
This keeps the pool unlocked (shares remain stable) and allows dynamic reward distribution dependent on external state (locked ve tokens, NFT badges, staking age).
Pattern: store base shares + compute boost in claim
module staking::boosted {
use staking::simple;
use sui::tx_context::TxContext;
// Example: a boost oracle module (off-chain/other module) provides multiplier
public(fun) native fun get_boost(owner: address): u128;
public(entry) fun claim_with_boost(share: &mut simple::Share, pool: &simple::Pool) {
// basic pending computed on base shares
let base_pending = (pool.acc_reward_per_share * share.shares / simple::PRECISION) - share.reward_debt;
// fetch boost multiplier (e.g., 120% => multiplier = 120_000_000 where scale = 1_000_000)
let boost = get_boost(share.owner);
// effective pending = base_pending * boost / BOOST_SCALE
let BOOST_SCALE: u128 = 1_000_000u128;
let effective = base_pending * boost / BOOST_SCALE;
// transfer effective to owner; update debt using base shares
share.reward_debt = pool.acc_reward_per_share * share.shares / simple::PRECISION;
}
}
Notes
- I make
get_boost
an oracle module or an on-chainVeToken
module that derives boost from lock amount & lock duration. - Because boost is applied at claim-time, you can change boost rules cheaply and retroactively (affects future claims only).
3) Virtual balances & strategy adapters (keep pools unlocked)
If your vault rebalances across strategies (Aave, yield source A, B), you should keep a virtual accounting layer. Users own shares that represent a claim on the vault, while the vault swaps and migrates real assets across strategies.
I model each strategy as an adapter object with deposit
and withdraw
entrypoints. When migrating, I do small, idempotent batches.
Why I do this: users can withdraw shares without needing to know where assets are invested; strategies can be rewired off-chain.
4) Off-chain accrual + Merkle claims (micro rewards)
When rewards are tiny and frequent (every block), on-chain bookkeeping is too costly. I compute per-user accruals off-chain (streaming), then publish an epoch Merkle root on-chain. Users claim by presenting a Merkle proof.
Pros: cheap on-chain for high-frequency systems. Cons: requires trust assumptions about the root publisher or a dispute/fraud-prove mechanism.
On-chain Merkle claim pattern (sketch)
module staking::merkle {
use sui::object::{UID};
use sui::tx_context::TxContext;
struct EpochRoot has key { id: UID, epoch: u64, root: vector<u8>, finalized: bool }
public(entry) fun publish_root(epoch: u64, root: vector<u8>, ctx: &mut TxContext): EpochRoot {
EpochRoot { id: object::new(ctx), epoch, root, finalized: false }
}
public(entry) fun claim_with_proof(root_obj: &EpochRoot, leaf: vector<u8>, proof: vector<vector<u8>>) {
assert!(verify_merkle_proof(leaf, proof, root_obj.root), 1);
// decode leaf -> (user, amount) and transfer amount
}
native fun verify_merkle_proof(leaf: vector<u8>, proof: vector<vector<u8>>, root: vector<u8>): bool;
}
I combine this with a short dispute window and on-chain challenger bounty to reduce trust.
5) Reward policy objects (pluggable rules)
I make the distribution logic a policy object: RewardPolicy
with variants (pro-rata, weighted by stake-age, exponential decay). The distributor reads the policy and updates acc_reward_per_share
.
This lets governance change reward curves without migrating per-user state.
struct RewardPolicy has key { id: UID, kind: u8 /* 0=pro-rata,1=age-weighted */, params: vector<u8> }
public(entry) fun distribute(policy: &RewardPolicy, pools: vector<&mut Pool>, reward: u128) {
match policy.kind { /* compute distribution */ }
// update pool.acc_reward_per_share accordingly
}
I prefer this architecture because it separates accounting (stable) and policy (evolvable).
6) Emergency withdrawal behavior (non-lock UX)
If strategies become illiquid, users must still be able to withdraw. I implement:
- Fast withdraw for liquid portion (immediate),
- Queued withdrawals for remainder (filled as strategies unwind),
- Emergency redeem with a small penalty.
This preserves UX without locking pools.
7) Full example: combine accRewardPerShare
+ boost + lazy claim
A compact end-to-end Move sketch (synthesis):
module staking::vault {
use staking::simple;
use staking::boosted;
use sui::object::{UID};
use sui::tx_context::TxContext;
const PRECISION: u128 = 1_000_000_000u128;
const BOOST_SCALE: u128 = 1_000_000u128;
// deposit_rewards, deposit, withdraw as in simple module...
public(entry) fun claim_with_boost(share: &mut simple::Share, pool: &simple::Pool) {
// compute base pending in integer arithmetic carefully
let base_pending = (pool.acc_reward_per_share * share.shares / PRECISION) - share.reward_debt;
let boost = boosted::get_boost(share.owner); // e.g., 1_200_000 => 1.2x
let total = base_pending * boost / BOOST_SCALE;
// transfer `total` to share.owner
share.reward_debt = pool.acc_reward_per_share * share.shares / PRECISION;
}
}
8) Audit & testing checklist (what I do)
- Invariant tests: total rewards distributed <= total rewards deposited.
sum_user_pending + pool.buffer == total_rewards_added
. - Rounding tests: simulate edge cases where
acc_reward_per_share
truncation could leave dust; provide dust-faucet or accumulate to next epoch. - Boost changes test: change boost function and ensure no retroactive double-dips.
- Migration tests: strategy adapter swaps; ensure rebalances don't lose funds.
- Fuzzing: random deposits, withdraws, reward deposits; compare on-chain accounting vs reference off-chain model.
- Concurrency tests: many parallel deposits/claims across disjoint pools to ensure no unnecessary serialization.
9) Operational notes & best practices (from my deployments)
- Keep accumulator updates atomic & minimal — one call per reward event.
- Prefer
Share
as an owned object per user so claims are parallelizable. - Add small stability buffer for edge rounding (collect dust into
reserve
and distribute periodically). - Document policy changes and use
RewardPolicy
objects so front-end can detect distribution rule changes. - Provide clear UX: show users their
pending
(computed off-chain by reading pool.accReward and user reward_debt) and expected gas for claim.
TL;DR — my recommended stack
- Core: shares +
accRewardPerShare
for each asset pool (lazy claims). - Add: boost multipliers applied at claim time (ve-token / tenure) to be dynamic and non-locking.
- For ultra-high-frequency micro rewards: off-chain accrual + Merkle claims.
- Always: policy objects, emergency withdraw, and rigorous tests (invariants, rounding, concurrency).
When I design multi-asset staking on Sui, I treat stakes as composable objects rather than locked balances. This lets stakers retain custody and move assets, while my staking module just tracks reward weights.
The main strategy I use is decoupling accounting from custody:
- Position NFTs: Every stake position is an object (
StakePosition
) that stores(asset_type, amount, weight, entry_epoch)
. - Reward weights: Instead of fixing APR, I compute
reward_weight = f(asset_type, volatility, pool_utilization)
dynamically every epoch. - Epoch snapshots: At the end of each epoch, I snapshot total weights across assets, then proportionally distribute rewards.
The trick to not lock underlying pools is delegated accounting: the actual LP tokens or coins can remain free in DeFi usage, while users mint a staking receipt object proving their commitment. Rewards accrue on the receipt object, not by immobilizing the asset.
Move-style snippet:
struct StakePosition<AssetType> has key {
id: UID,
owner: address,
amount: u64,
entry_epoch: u64,
weight: u64,
}
public entry fun stake<AssetType>(
coin: Coin<AssetType>,
ctx: &mut TxContext
): StakePosition<AssetType> {
let amount = coin::value(&coin);
let weight = compute_weight::<AssetType>(amount);
coin::burn(coin); // burn in exchange for stake receipt
StakePosition { id: object::new(ctx), owner: tx_context::sender(ctx), amount, entry_epoch: epoch_now(), weight }
}
By burning and re-minting receipts, I decouple staking logic from liquidity use. With this pattern, rewards adjust every epoch using a dynamic weight function (e.g., higher weight for volatile or scarce assets).
This makes multi-asset staking fluid and composable, without hard locks.
As a professional Web3 developer specializing in Sui and Move, I'll outline my approach to implementing dynamic multi-asset staking pools that maintain flexibility while ensuring fair reward distribution.
Core Architecture
The foundation of my implementation relies on a modular design that separates concerns while maintaining flexibility:
struct StakingPool {
assets: map<address, AssetInfo>,
stakes: map<address, StakeInfo>,
rewards: map<address, RewardInfo>,
config: PoolConfig,
}
struct AssetInfo {
id: ID,
decimals: u8,
reward_multiplier: uint128,
min_stake: uint128,
}
struct StakeInfo {
principal: uint128,
reward_debt: uint128,
last_update: u64,
asset_type: address,
}
flowchart TD
classDef core fill:#FF9999,stroke:#CC0000,color:#000
classDef parallel fill:#99FF99,stroke:#00CC00,color:#000
classDef storage fill:#9999FF,stroke:#0000CC,color:#000
classDef oracle fill:#FFFF99,stroke:#CCCC00,color:#000
subgraph Core["Core Components"]
Pool["Pool Manager"]:::core
PTB["Programmable Transaction Blocks"]:::core
end
subgraph Parallel["Parallel Execution Layer"]
PE["Execution Engine"]:::parallel
MEV["MEV Protection"]:::parallel
DAG["DAG-based Consensus"]:::parallel
end
subgraph Storage["Object Storage"]
Assets["Asset Objects"]:::storage
LP["LP Tokens"]:::storage
Metadata["Pool Metadata"]:::storage
end
subgraph Oracle["Price Feeds"]
PO["Price Oracles"]:::oracle
Cache["Price Cache"]:::oracle
end
Pool --> PTB
PTB --> PE
PE --> MEV
PE --> DAG
Pool --> Assets
Pool --> LP
Pool --> Metadata
Assets <--> PO
PO --> Cache
%% Legend
subgraph Legend["Legend"]
C1["Core Components"]:::core
P1["Parallel Processing"]:::parallel
S1["Storage Layer"]:::storage
O1["Oracle System"]:::oracle
end
The diagram above illustrates the system architecture, where:
- Red components represent core system elements handling pool operations
- Green sections show the parallel processing infrastructure
- Blue represents object storage for assets and metadata
- Yellow indicates external price feed integration
Implementation Strategy
-
Dynamic Reward Distribution```move public entry fun calculate_rewards( staker: address, asset_type: address, ctx: &mut TxContext ): uint128 { let pool = borrow_global_mut
(POOL_ID); let stake = pool.stakes.get(&staker).unwrap(); let asset = pool.assets.get(&asset_type).unwrap(); // Calculate rewards based on multiple factors let base_reward = calculate_base_reward(stake.principal); let multiplier = asset.reward_multiplier; let time_factor = calculate_time_factor(stake.last_update);
return base_reward * multiplier * time_factor; }
2. **Non-Locking Mechanism**```move
public entry fun stake(
asset_type: address,
amount: uint128,
ctx: &mut TxContext
) {
let pool = borrow_global_mut<StakingPool>(POOL_ID);
let staker = signer_from_context(ctx);
// Record stake without locking
pool.stakes.insert(
&staker,
StakeInfo {
principal: amount,
reward_debt: 0,
last_update: current_timestamp(),
asset_type,
}
);
// Emit event for external tracking
emit_event!(StakeEvent {
staker,
amount,
asset_type,
timestamp: current_timestamp(),
});
}
Security Considerations
- Protection Mechanisms
- Implement flash loan prevention through temporal locks
- Use Move's formal verification for critical paths
- Maintain separate price feeds for different asset pairs
- Gas Efficiency
- Batch similar operations within transaction blocks
- Utilize sponsored transactions for common operations
- Implement lazy state updates where possible
Performance Optimization
- Scaling Strategy
- Split large staking operations across multiple blocks
- Implement dynamic fee adjustment based on network congestion
- Cache frequently accessed data in memory
- Monitoring and Maintenance
- Track pool utilization metrics
- Monitor gas costs per operation
- Adjust reward parameters based on performance data
This implementation provides several key benefits:
- Flexible reward distribution without pool locking
- Efficient parallel execution through modular design
- Enhanced security through formal verification
- Scalable architecture for multiple assets
Thoroughly test the reward distribution patterns under various load conditions before deployment, as the optimal configuration may vary depending on your specific use case and expected volume.
Focusing on a more event-driven and modular architecture that emphasizes flexibility and scalability.
Core Architecture
The foundation of this implementation centers around a modular design that separates concerns while maintaining flexibility:
struct StakingPool {
assets: map<address, AssetInfo>,
stakes: map<address, StakeInfo>,
rewards: map<address, RewardInfo>,
config: PoolConfig,
}
struct AssetInfo {
id: ID,
decimals: u8,
reward_multiplier: uint128,
min_stake: uint128,
}
struct StakeInfo {
principal: uint128,
reward_debt: uint128,
last_update: u64,
asset_type: address,
}
flowchart TD
classDef core fill:#FF9999,stroke:#CC0000,color:#000
classDef parallel fill:#99FF99,stroke:#00CC00,color:#000
classDef storage fill:#9999FF,stroke:#0000CC,color:#000
classDef oracle fill:#FFFF99,stroke:#CCCC00,color:#000
subgraph Core["Core Components"]
Pool["Pool Manager"]:::core
PTB["Programmable Transaction Blocks"]:::core
end
subgraph Parallel["Parallel Execution Layer"]
PE["Execution Engine"]:::parallel
MEV["MEV Protection"]:::parallel
DAG["DAG-based Consensus"]:::parallel
end
subgraph Storage["Object Storage"]
Assets["Asset Objects"]:::storage
LP["LP Tokens"]:::storage
Metadata["Pool Metadata"]:::storage
end
subgraph Oracle["Price Feeds"]
PO["Price Oracles"]:::oracle
Cache["Price Cache"]:::oracle
end
Pool --> PTB
PTB --> PE
PE --> MEV
PE --> DAG
Pool --> Assets
Pool --> LP
Pool --> Metadata
Assets <--> PO
PO --> Cache
%% Legend
subgraph Legend["Legend"]
C1["Core Components"]:::core
P1["Parallel Processing"]:::parallel
S1["Storage Layer"]:::storage
O1["Oracle System"]:::oracle
end
The diagram above illustrates the system architecture, where:
- Red components represent core system elements handling pool operations
- Green sections show the parallel processing infrastructure
- Blue represents object storage for assets and metadata
- Yellow indicates external price feed integration
Implementation Strategy
-
Dynamic Reward Distribution```move public entry fun calculate_rewards( staker: address, asset_type: address, ctx: &mut TxContext ): uint128 { let pool = borrow_global_mut
(POOL_ID); let stake = pool.stakes.get(&staker).unwrap(); let asset = pool.assets.get(&asset_type).unwrap(); // Calculate rewards based on multiple factors let base_reward = calculate_base_reward(stake.principal); let multiplier = asset.reward_multiplier; let time_factor = calculate_time_factor(stake.last_update);
return base_reward * multiplier * time_factor; }
2. **Non-Locking Mechanism**```move
public entry fun stake(
asset_type: address,
amount: uint128,
ctx: &mut TxContext
) {
let pool = borrow_global_mut<StakingPool>(POOL_ID);
let staker = signer_from_context(ctx);
// Record stake without locking
pool.stakes.insert(
&staker,
StakeInfo {
principal: amount,
reward_debt: 0,
last_update: current_timestamp(),
asset_type,
}
);
// Emit event for external tracking
emit_event!(StakeEvent {
staker,
amount,
asset_type,
timestamp: current_timestamp(),
});
}
Security Considerations
- Protection Mechanisms
- Implement flash loan prevention through temporal locks
- Use Move's formal verification for critical paths
- Maintain separate price feeds for different asset pairs
- Gas Efficiency
- Batch similar operations within transaction blocks
- Utilize sponsored transactions for common operations
- Implement lazy state updates where possible
Performance Optimization
- Scaling Strategy
- Split large staking operations across multiple blocks
- Implement dynamic fee adjustment based on network congestion
- Cache frequently accessed data in memory
- Monitoring and Maintenance
- Track pool utilization metrics
- Monitor gas costs per operation
- Adjust reward parameters based on performance data
This implementation provides several key benefits:
- Flexible reward distribution without pool locking
- Efficient parallel execution through modular design
- Enhanced security through formal verification
- Scalable architecture for multiple assets
Thoroughly test the reward distribution patterns under various load conditions before deployment, as the optimal configuration may vary depending on your specific use case and expected volume.
You can implement multi-asset, non-locking staking on Sui by tokenizing each staked asset into liquid staking tokens (LSTs) and running a pooled reward-engine that uses per-share (virtual balance) accounting plus a dynamic allocator that weights rewards by real-time asset value and protocol objectives; specifically, mint an LST when a user deposits (so their underlying stake keeps earning on-chain via Sui’s stake objects/SIP-6) and track each holder’s cumulative rewards using a reward-per-share index to avoid locking assets, feed in a price oracle (or TWAP) to convert different asset yields into a common unit, run an epochal rebalancer that updates allocation curves (for example increasing rewards to under-utilized assets or reducing rewards when an LST peg diverges), optionally add incentive curves or decay functions to steer capital and protect against gaming, keep withdrawal/redemption as an off-chain or on-chain burn+redeem that unwraps underlying stake without forcing long locks (use queuing or buffered redemption if chain unbonding imposes delays), enforce risk controls (slashing caps, peg monitoring, and limits per validator) and expose governance parameters so you can tune distribution rules; architect the core accounting and reward logic in Move modules so calculations are atomic and auditable, use separate accounting for protocol fees vs. gross staking rewards, and simulate reward flows before mainnet launch to tune oracle latency, rebalancing cadence and economic safety margins.
你知道答案吗?
请登录并分享。
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.