Spool Staking
Stake market coins in Scallop spool to earn additional SCA rewards.
Overview
Spool is Scallop's staking system where depositors can stake their market coins to earn:
- Base Yield: Interest from lending
- SCA Rewards: Additional SCA token rewards
Note: This skill uses
supply()to supply assets to the lending pool before staking.
How Spool Works
1. Deposit assets → Receive market coins
2. Stake market coins in spool → Earn SCA rewards
3. Claim rewards anytime
4. Unstake when desired → Get market coins back
Getting Started
Create Stake Account
Each asset requires a separate stake account. You need the stake_pool_id for that asset (from protocol addresses):
tx = builder.create_tx_block()
# Create stake account for SUI market coins
# stake_pool_id comes from protocol addresses config
stake_account_idx = tx.create_stake_account(
stake_pool_id=stake_pool_id, # Stake pool object ID
coin_name="sui",
)
# Transfer stake account to yourself
tx.transfer_objects([stake_account_idx], wallet_address)
result = builder.sign_and_send_tx_block(tx)
# Parse stake_account_id from result
// TypeScript - Create stake account
const client = await scallop.createScallopClient();
const result = await client.createStakeAccount('sui');
// Parse stakeAccountId from result
Stake Market Coins
tx = builder.create_tx_block()
# Option 1: Deposit and stake in one transaction
market_coin = tx.supply_quick(1_000_000_000, "sui", wallet_address)
tx.stake_spool(
stake_account_id=stake_account_id, # Stake account object ID
stake_pool_id=stake_pool_id, # Stake pool object ID
coin_input_idx=market_coin, # Index of market coin to stake
coin_name="sui",
)
# Option 2: Stake existing market coins
# market_coin_idx = tx.add_object_input("0xMARKET_COIN_OBJECT_ID")
# tx.stake_spool(stake_account_id, stake_pool_id, market_coin_idx, "sui")
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Deposit and stake
const client = await scallop.createScallopClient();
const result = await client.depositAndStake('sui', 1_000_000_000, true, stakeAccountId);
// Or stake existing market coins
const tx = builder.createTxBlock();
await tx.stakeQuick(1_000_000_000, 'sui', stakeAccountId);
await builder.signAndSendTxBlock(tx);
Claim Rewards
tx = builder.create_tx_block()
# Claim accumulated SCA rewards
reward_coin = tx.claim_spool_rewards(
stake_account_id=stake_account_id, # Stake account object ID
stake_pool_id=stake_pool_id, # Stake pool object ID
reward_pool_id=reward_pool_id, # Reward pool object ID
coin_name="sui",
)
tx.transfer_objects([reward_coin], wallet_address)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Claim rewards
const client = await scallop.createScallopClient();
const result = await client.claim('sui', true, stakeAccountId);
// Or using builder
const tx = builder.createTxBlock();
const rewards = await tx.claimQuick('sui', stakeAccountId);
tx.transferObjects(rewards, sender);
await builder.signAndSendTxBlock(tx);
Unstake
tx = builder.create_tx_block()
# Unstake market coins
market_coin = tx.unstake_spool(
stake_account_id=stake_account_id, # Stake account object ID
stake_pool_id=stake_pool_id, # Stake pool object ID
amount=1_000_000_000, # Amount of market coins to unstake
coin_name="sui",
)
# Option 1: Keep market coins
tx.transfer_objects([market_coin], wallet_address)
# Option 2: Withdraw to underlying
underlying = tx.withdraw(market_coin, "sui", coin_is_result=True)
tx.transfer_objects([underlying], wallet_address)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Unstake
const client = await scallop.createScallopClient();
const result = await client.unstake('sui', 1_000_000_000, true, stakeAccountId);
// Or unstake and withdraw in one call
const result = await client.unstakeAndWithdraw('sui', 1_000_000_000, true, stakeAccountId);
// Using builder with unstakeQuick
const tx = builder.createTxBlock();
const sCoin = await tx.unstakeQuick(1_000_000_000, 'sui', stakeAccountId);
tx.transferObjects([sCoin], sender);
await builder.signAndSendTxBlock(tx);
Complete Staking Flow
from sui_scallop_sdk import ScallopClient
client = ScallopClient(secret_key="...", network="mainnet")
builder = client.create_builder()
# Step 1: Create stake account (one-time)
tx1 = builder.create_tx_block()
stake_account = tx1.create_stake_account(stake_pool_id, "sui")
tx1.transfer_objects([stake_account], client.wallet_address)
result1 = builder.sign_and_send_tx_block(tx1)
stake_account_id = parse_created_object(result1)
# Step 2: Deposit and stake
tx2 = builder.create_tx_block()
market_coin = tx2.supply_quick(10_000_000_000, "sui", client.wallet_address)
tx2.stake_spool(stake_account_id, stake_pool_id, market_coin, "sui")
result2 = builder.sign_and_send_tx_block(tx2)
# Step 3: Wait and accumulate rewards...
# Step 4: Claim rewards
tx3 = builder.create_tx_block()
rewards = tx3.claim_spool_rewards(stake_account_id, stake_pool_id, reward_pool_id, "sui")
tx3.transfer_objects([rewards], client.wallet_address)
result3 = builder.sign_and_send_tx_block(tx3)
# Step 5: Unstake when ready
tx4 = builder.create_tx_block()
market_coin = tx4.unstake_spool(stake_account_id, stake_pool_id, 10_000_000_000, "sui")
underlying = tx4.withdraw(market_coin, "sui", coin_is_result=True)
tx4.transfer_objects([underlying], client.wallet_address)
result4 = builder.sign_and_send_tx_block(tx4)
Query Staking Position
query = client.create_query()
# Get stake account object data (raw object query — no dedicated stake query method yet)
stake_data = query.get_object(stake_account_id)
# Parse fields from content
fields = stake_data.get("content", {}).get("fields", {})
print(f"Stake Account: {stake_account_id}")
print(f"Fields: {fields}")
Note: The Python SDK (
sui-scallop-sdk>= 0.3.0a1) also has dedicated spool reads onScallopQuery:get_spool(market_coin_name)/get_spools()for pool state andget_stake_accounts()/get_all_stake_accounts()for a wallet's stake accounts — prefer these over rawget_object()parsing.
Supported Spool Markets
Only 9 of the 34 lending markets have a spool: ssui, susdc, swusdc, swusdt, sweth,
scetus, safsui, shasui, svsui (keyed by market coin). Check before assuming one exists.
APY is variable — it depends on the pool's emission rate and total staked, so query it with
query.get_spool(market_coin_name) rather than assuming a figure.
Full table with stake pool and reward pool IDs: staking-mechanics.md.
Multi-Asset Staking
Stake multiple assets for diversified yield:
assets_to_stake = [
("sui", 10_000_000_000),
("usdc", 1_000_000_000),
("weth", 50_000_000)
]
for coin_name, amount in assets_to_stake:
# Get pool IDs for this coin from protocol addresses
pool_id = get_stake_pool_id(coin_name) # from addresses config
# Create stake account
tx1 = builder.create_tx_block()
stake_account = tx1.create_stake_account(pool_id, coin_name)
tx1.transfer_objects([stake_account], wallet_address)
result1 = builder.sign_and_send_tx_block(tx1)
stake_account_id = parse_created_object(result1)
# Deposit and stake
tx2 = builder.create_tx_block()
market_coin = tx2.supply_quick(amount, coin_name, wallet_address)
tx2.stake_spool(stake_account_id, pool_id, market_coin, coin_name)
builder.sign_and_send_tx_block(tx2)
print(f"Staked {amount} {coin_name}")
Auto-Compound Strategy
Rewards do not auto-compound — unclaimed rewards earn nothing, so reinvesting is a loop you run yourself: claim → swap to the staked asset → supply → stake the market coin.
Full worked loop, interval trade-offs, and failure handling: rewards-calculation.md.
Per-Reward-Type Redemption (Move)
The high-level helpers above call into spool::user::redeem_rewards<StakeType, RewardType> under the hood. A spool can have multiple reward types (e.g. SCA + a campaign token), so the Move entrypoint is generic in both the staked market coin and the reward coin:
public fun redeem_rewards<StakeType, RewardType>(
spool: &mut Spool,
rewards_pool: &mut RewardsPool<RewardType>,
spool_account: &mut SpoolAccount<StakeType>,
clock: &Clock,
ctx: &mut TxContext,
): Coin<RewardType>
The shared Spool is reward-agnostic; the RewardsPool is generic over RewardType only. Argument order matters — rewards_pool sits between spool and spool_account.
Source: spool/spool/sources/user.move.
When to drop down to the raw move call
- The reward pool you want to claim isn't the default SCA pool the SDK helper targets (e.g. a temporary campaign reward pool).
- You want to claim multiple reward types for the same stake account in one PTB.
- You're building an indexer/keeper and need to enumerate
RewardsPoolobjects via rpc-client, then callredeem_rewardsper pair.
Example: Multi-Reward Claim (TypeScript)
const tx = builder.createTxBlock();
// SCA rewards
const sca = tx.moveCall({
target: `${spoolPkg}::user::redeem_rewards`,
typeArguments: [marketCoinType, scaType],
arguments: [
tx.object(spoolId),
tx.object(scaRewardsPoolId),
tx.object(stakeAccountId),
tx.object('0x6'),
],
});
// Campaign reward (different RewardType, same shared Spool + stake account)
const bonus = tx.moveCall({
target: `${spoolPkg}::user::redeem_rewards`,
typeArguments: [marketCoinType, bonusRewardType],
arguments: [
tx.object(spoolId),
tx.object(bonusRewardsPoolId),
tx.object(stakeAccountId),
tx.object('0x6'),
],
});
tx.transferObjects([sca, bonus], sender);
await builder.signAndSendTxBlock(tx);
The SDK's claim_spool_rewards() / claimQuick() helpers internally use this for the canonical SCA pool only.
Error Handling
| Error | Cause | Solution |
|---|---|---|
SpoolNotFound |
Invalid coin name | Check spool exists for asset |
InvalidStakeAccount |
Wrong account | Use correct stake account |
InsufficientStake |
Not enough staked | Reduce unstake amount |
NoRewardsToClaim |
No pending rewards | Wait for rewards to accrue |
RewardPoolMismatch |
redeem_rewards called with a RewardsPool that doesn't match the SpoolPool's StakeType |
Verify the rewards pool object ID belongs to the same spool |
References
- Staking Mechanics - How spool works
- Rewards Calculation - Reward formulas
- Supported Coins - All supported assets
- Advanced Transactions - Raw moveCall patterns