Borrow Incentive
Earn additional rewards for borrowing on Scallop by staking your obligation.
Overview
Borrow Incentive program rewards borrowers with SCA tokens. By staking your obligation, you earn rewards proportional to your borrow amount.
How It Works
1. Create obligation and borrow assets
2. Stake obligation in borrow incentive program
3. Accumulate SCA rewards over time
4. Claim rewards anytime
5. Unstake when desired
Getting Started
Stake Obligation
tx = builder.create_tx_block()
# Stake your obligation for borrow incentives
tx.stake_obligation(
obligation_id=obligation_id,
obligation_key=obligation_key
)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Stake obligation
const client = await scallop.createScallopClient();
const result = await client.stakeObligation(obligationId, obligationKeyId);
// Or using builder
const tx = builder.createTxBlock();
await tx.stakeObligationWithVeScaQuick(obligationId, obligationKeyId);
await builder.signAndSendTxBlock(tx);
Claim Rewards
tx = builder.create_tx_block()
# Claim borrow incentive rewards for specific debt asset
reward_coin = tx.claim_borrow_incentive(
obligation_id=obligation_id,
obligation_key=obligation_key,
coin_name="usdc" # The debt asset you're claiming rewards for
)
tx.transfer_objects([reward_coin], wallet_address)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Claim borrow incentive rewards
const client = await scallop.createScallopClient();
const result = await client.claimBorrowIncentive(obligationId, obligationKeyId);
// Or using builder
const tx = builder.createTxBlock();
const rewardCoin = await tx.claimBorrowIncentiveQuick('sca', obligationId, obligationKeyId);
tx.transferObjects([rewardCoin], sender);
await builder.signAndSendTxBlock(tx);
Unstake Obligation
tx = builder.create_tx_block()
# Unstake obligation
tx.unstake_obligation(
obligation_id=obligation_id,
obligation_key=obligation_key
)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Unstake obligation
const client = await scallop.createScallopClient();
const result = await client.unstakeObligation(obligationId, obligationKeyId);
// Or using builder
const tx = builder.createTxBlock();
await tx.unstakeObligationQuick(obligationId, obligationKeyId);
await builder.signAndSendTxBlock(tx);
Complete Flow
from sui_scallop_sdk import ScallopClient
client = ScallopClient(secret_key="...", network="mainnet")
builder = client.create_builder()
# Assume obligation already exists with borrows
# Step 1: Stake obligation
tx1 = builder.create_tx_block()
tx1.stake_obligation(obligation_id, obligation_key)
builder.sign_and_send_tx_block(tx1)
print("Obligation staked for borrow incentives")
# Step 2: Wait and accumulate rewards...
# Step 3: Claim rewards (for each debt asset)
tx2 = builder.create_tx_block()
usdc_rewards = tx2.claim_borrow_incentive(obligation_id, obligation_key, "usdc")
tx2.transfer_objects([usdc_rewards], client.wallet_address)
builder.sign_and_send_tx_block(tx2)
print("Claimed USDC borrow incentive rewards")
# Step 4: Unstake when done
tx3 = builder.create_tx_block()
tx3.unstake_obligation(obligation_id, obligation_key)
builder.sign_and_send_tx_block(tx3)
print("Obligation unstaked")
Query Incentive Status
Note: As of
sui-scallop-sdk0.3.0a1, the Python SDK's borrow-incentive reads are limited to veSCA binding lookups (get_binded_obligation_id,get_binded_vesca_key) — there are no pool/account/pending-reward queries. For those, use the TS SDK'sgetBorrowIncentivePools()/getBorrowIncentiveAccounts()(v4.3.0) or raw object queries.
Reward Calculation
Rewards are distributed based on:
- Your borrow amount relative to total staked borrows
- Time duration
- Current reward rate
your_share = your_borrow_value / total_staked_borrows
your_rewards = total_rewards * your_share * time_elapsed
Effective Borrow Rate
With borrow incentives, your effective borrow rate is reduced:
Effective APY = Borrow APY - Reward APY
Example:
Borrow APY: 8.5%
Reward APY: 3.2%
Effective APY: 5.3% (or even negative = paid to borrow!)
Note: Market and incentive APY data is not yet queryable via the Python SDK. Use the TypeScript SDK or Scallop app for current rates.
The two terms are not the same kind of number: borrow APY is a certain cost in the borrowed asset, while reward APY is SCA-denominated income whose value depends on the SCA price when you sell. A negative effective APY is real but is not a locked-in free carry. See incentive-mechanics.md.
Supported Assets
Incentive coverage and reward rates are protocol parameters that change as programs are funded and
retired — there is no fixed list. Read the current set from chain with the TS SDK's
getBorrowIncentivePools() rather than assuming a table.
Multi-Asset Rewards
If you have multiple debts, claim rewards for each:
tx = builder.create_tx_block()
# Get your debt assets
obligation = query.get_obligation(obligation_id)
debt_assets = [d.coin_name for d in obligation.debts]
# Claim rewards for each
rewards = []
for coin_name in debt_assets:
reward = tx.claim_borrow_incentive(obligation_id, obligation_key, coin_name)
rewards.append(reward)
# Transfer all rewards
tx.transfer_objects(rewards, wallet_address)
result = builder.sign_and_send_tx_block(tx)
Auto-Claim Strategy
Automatically claim rewards periodically:
import time
def auto_claim_borrow_incentives(client, obligation_id, obligation_key, interval_hours=24):
"""Auto-claim borrow incentives."""
builder = client.create_builder()
query = client.create_query()
while True:
try:
obligation = query.get_obligation(obligation_id)
if obligation.debts:
tx = builder.create_tx_block()
for debt in obligation.debts:
reward = tx.claim_borrow_incentive(
obligation_id,
obligation_key,
debt.coin_name
)
tx.transfer_objects([reward], client.wallet_address)
builder.sign_and_send_tx_block(tx)
print(f"Claimed borrow incentives")
except Exception as e:
print(f"Error: {e}")
time.sleep(interval_hours * 3600)
Combining with Spool Staking
Double rewards strategy:
- Deposit: Earn lending interest
- Stake in Spool: Earn spool SCA rewards
- Borrow: Use different assets as collateral
- Stake Obligation: Earn borrow incentive rewards
# Combined strategy
tx = builder.create_tx_block()
# Deposit and stake in spool
market_coin = tx.supply_quick(10_000_000_000, "sui", wallet)
tx.stake_spool(stake_account_id, stake_pool_id, market_coin, "sui")
# Add collateral and borrow
tx.deposit_collateral_quick(5_000_000_000, "sui", obligation_id, wallet)
borrowed = tx.borrow_quick(100_000_000, "usdc", obligation_id, obligation_key)
tx.transfer_objects([borrowed], wallet)
# Stake obligation for borrow incentives
tx.stake_obligation(obligation_id, obligation_key)
result = builder.sign_and_send_tx_block(tx)
# Now earning: lending yield + spool rewards + borrow incentives!
Keeper / Liquidator Entry Points
borrow-incentive-v2 exposes a handful of entry functions in borrow_incentive/sources/user.move that aren't part of the normal user flow but matter for keepers, liquidators and indexers:
| Move entrypoint | Purpose |
|---|---|
force_unstake_unhealthy_v3 |
Anyone can unstake an obligation from the incentive pool once it crosses into unhealthy territory. Pair with a liquidation in the same PTB. (force_unstake_unhealthy / _v2 still exist in the module but are deprecated abort 0 stubs — calling them always fails; they are not a backward-compat path.) |
refresh_inactive_boost |
Re-evaluates the obligation's veSCA-derived boost when the underlying veSCA position changed (e.g. lock extended, key transferred). Needed before claims pay out at the new boost. |
deactivate_boost_v2 |
Removes a veSCA boost binding (public fun, not entry) — used during obligation closures. |
Force-Unstake an Unhealthy Obligation (TypeScript)
The full v3 signature (user.move) takes the incentive triplet plus the lending-protocol objects needed to evaluate health and the veSCA subscriber tables needed to unbind any boost:
force_unstake_unhealthy_v3(
incentive_config: &IncentiveConfig,
incentive_pools: &mut IncentivePools,
incentive_accounts: &mut IncentiveAccounts,
protocol_version: &Version,
obligation: &mut Obligation,
market: &mut Market,
coin_decimals_registry: &CoinDecimalsRegistry,
x_oracle: &XOracle,
ve_sca_subs_table: &mut VeScaSubscriberTable,
ve_sca_subs_whitelist: &VeScaSubscriberWhitelist,
clock: &Clock,
ctx: &mut TxContext,
)
const tx = builder.createTxBlock();
tx.moveCall({
target: `${borrowIncentivePkg}::user::force_unstake_unhealthy_v3`,
arguments: [
tx.object(borrowIncentiveConfigId),
tx.object(incentivePoolsId),
tx.object(incentiveAccountsId),
tx.object(protocolVersionId),
tx.object(targetObligationId),
tx.object(marketId),
tx.object(coinDecimalsRegistryId),
tx.object(xOracleId),
tx.object(veScaSubscriberTableId),
tx.object(veScaSubscriberWhitelistId),
tx.object('0x6'), // clock
],
});
// Same PTB can then run a liquidate() against targetObligationId.
await builder.signAndSendTxBlock(tx);
All object IDs except clock come from ScallopAddress — e.g. address.get('core.version'), address.get('core.market'), address.get('core.coinDecimalsRegistry'), address.get('core.oracles.xOracle'), and the borrowIncentive.* and vesca.subscriber.* fields.
Refresh Boost After veSCA Change
Full signature:
refresh_inactive_boost(
incentive_config: &IncentiveConfig,
incentive_pools: &mut IncentivePools,
incentive_accounts: &mut IncentiveAccounts,
ve_sca_table: &VeScaTable,
obligation: &mut Obligation,
clock: &Clock,
ctx: &mut TxContext,
)
tx.moveCall({
target: `${borrowIncentivePkg}::user::refresh_inactive_boost`,
arguments: [
tx.object(borrowIncentiveConfigId),
tx.object(incentivePoolsId),
tx.object(incentiveAccountsId),
tx.object(veScaTableId),
tx.object(obligationId),
tx.object('0x6'), // clock
],
});
Note: this entrypoint takes the Obligation (not its key) and the shared VeScaTable (not the user's VeScaKey). It asserts the bound veSCA's current power is zero before unbinding — i.e. it's specifically for cleaning up a stale boost after the lock expired.
The TS SDK wraps
deactivate_boost_v2as the builder methoddeactivateBoost(obligation, veScaKey)(seesrc/txBuilders/borrowIncentive/moveCalls.ts).force_unstake_unhealthy_v3andrefresh_inactive_boostare not wrapped by either SDK — call them viamoveCall(advanced-transactions). Thestake_with_ve_sca_v2entrypoint (also in the same module) is the recommended path for boosted staking and is wrapped by the TSstakeObligationWithVeScaQuickhelper.
Error Handling
| Error | Cause | Solution |
|---|---|---|
ObligationNotStaked |
Obligation not in program | Stake first |
NoBorrowsToIncentivize |
No active borrows | Borrow first |
NoRewardsToClaim |
No pending rewards | Wait for rewards |
ObligationStillHealthy |
force_unstake_unhealthy_* called against a healthy obligation |
Check obligation risk first |
BoostStillActive |
refresh_inactive_boost called when the boost is current |
No refresh needed |
References
- Incentive Mechanics - How incentives work
- Obligation Manager - Obligation basics
- Advanced Transactions - Raw moveCall patterns
- veSCA - Boost source