veSCA (Vote Escrow SCA)
Lock SCA tokens to receive veSCA for governance voting and protocol revenue sharing.
Overview
veSCA is Scallop's governance token system:
- Lock SCA → Receive veSCA
- veSCA → Voting power + Revenue share
- Longer lock → More voting power
Voting Power Formula
voting_power = locked_sca * (remaining_time / max_lock_time)
max_lock_time = 4 years (1461 days)
Power depends on the time remaining, not time served, and decays linearly to zero at expiry. Full derivation, the multiplier table, an off-chain calculator, and decay behaviour: voting-power.md.
Lock SCA
Create New Lock
tx = builder.create_tx_block()
# First, get a SCA coin input (e.g., split from existing coins)
sca_coin_idx = tx.add_object_input("0xSCA_COIN_OBJECT_ID")
# Lock SCA for 365 days
ve_sca_key = tx.lock_sca(
sca_coin_idx=sca_coin_idx, # Index of SCA coin (Input or Result)
lock_period_days=365, # Lock period in days (max 1461 = 4 years)
coin_is_result=False, # True if sca_coin_idx is a Result index
)
tx.transfer_objects([ve_sca_key], wallet_address)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Lock SCA
const tx = builder.createTxBlock();
const unlockAt = Math.floor(Date.now() / 1000) + (365 * 24 * 60 * 60); // 1 year
tx.lockSca(veScaKey, scaCoin, unlockAt);
await builder.signAndSendTxBlock(tx);
Lock Period Options
Maximum lock is 4 years (1461 days); the multiplier is period / 1461 days, so only a full
4-year lock gives 1:1. Full multiplier table:
voting-power.md.
Manage Lock
Extend Lock Period
Extend your lock to increase voting power:
tx = builder.create_tx_block()
# Extend lock by 6 months
new_unlock_timestamp = current_unlock + (86400 * 180) # 180 days
tx.extend_lock_period(
vesca_key_id=ve_sca_key_id,
new_unlock_at=new_unlock_timestamp
)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Extend lock period
const tx = builder.createTxBlock();
const newUnlockAt = currentUnlock + (180 * 24 * 60 * 60);
tx.extendLockPeriod(veScaKey, newUnlockAt);
await builder.signAndSendTxBlock(tx);
Extend Lock Amount
Add more SCA to existing lock:
tx = builder.create_tx_block()
# Add more SCA to existing lock
sca_coin_idx = tx.add_object_input("0xSCA_COIN_OBJECT_ID")
tx.extend_lock_amount(
vesca_key_id=ve_sca_key_id,
sca_coin_idx=sca_coin_idx, # Index of additional SCA coin
)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Extend lock amount
const tx = builder.createTxBlock();
tx.extendLockAmount(veScaKey, scaCoin);
await builder.signAndSendTxBlock(tx);
Renew Expired Lock
Re-lock expired veSCA:
tx = builder.create_tx_block()
# Renew expired lock with new SCA and new unlock timestamp
sca_coin_idx = tx.add_object_input("0xSCA_COIN_OBJECT_ID")
new_unlock_at = int(time.time()) + (365 * 86400) # 1 year in seconds
tx.renew_expired_vesca(
vesca_key_id=ve_sca_key_id,
sca_coin_idx=sca_coin_idx, # SCA coin to lock
new_unlock_at=new_unlock_at, # Unlock timestamp in seconds
)
result = builder.sign_and_send_tx_block(tx)
// TypeScript - Renew expired lock
const tx = builder.createTxBlock();
tx.renewExpiredVeSca(veScaKey);
await builder.signAndSendTxBlock(tx);
Redeem SCA
After lock expires, redeem your SCA:
tx = builder.create_tx_block()
# Redeem locked SCA (only works after unlock time)
sca_coin = tx.redeem_sca(vesca_key_id=ve_sca_key_id)
tx.transfer_objects([sca_coin], wallet_address)
result = builder.sign_and_send_tx_block(tx)
Query veSCA Position
query = client.create_query()
# Get veSCA object data (raw object query — no dedicated veSCA query method yet)
vesca_data = query.get_object(ve_sca_key_id)
# Parse fields from content
fields = vesca_data.get("content", {}).get("fields", {})
print(f"veSCA Object: {ve_sca_key_id}")
print(f"Fields: {fields}")
On-Chain Getter Helpers
For cases where you don't want to parse raw object JSON, the ve_sca Move module exposes three public read helpers that take the veSCA key ID, the shared VeScaTable, and (for current power) the Clock:
| Function | Returns | Purpose |
|---|---|---|
ve_sca::unlock_at(ve_sca_key_id, &ve_sca_table) |
u64 |
Lock expiry, Unix seconds |
ve_sca::locked_sca_amount(ve_sca_key_id, &ve_sca_table) |
u64 |
Total SCA currently locked |
ve_sca::ve_sca_amount(ve_sca_key_id, &ve_sca_table, &clock) |
u64 |
Current voting power (decayed) |
Source: ve-sca/sources/ve_sca.move. The VeScaTable is a shared object; its ID lives in protocol addresses (core.vesca.table).
Use in a Devnet-Inspect / Dry-Run
These are public fun (not entry), so the standard way to consume them off-chain is devInspectTransactionBlock on a PTB that calls them:
const tx = new TransactionBlock();
const power = tx.moveCall({
target: `${veScaPkg}::ve_sca::ve_sca_amount`,
arguments: [
tx.pure.id(veScaKeyId),
tx.object(veScaTableId),
tx.object('0x6'), // clock
],
});
tx.moveCall({ target: `${utilsPkg}::utils::emit_u64`, arguments: [power] }); // any sink
const r = await suiClient.devInspectTransactionBlock({
sender,
transactionBlock: tx,
});
// Parse the returned value from r.results
The same pattern works for unlock_at and locked_sca_amount — and is cheaper than fetching and parsing the raw veSCA object when you only need one scalar.
The Python and TypeScript SDKs don't currently wrap these getters. Use them when raw-object parsing becomes brittle (e.g. across module upgrades), or to fold a fresh, decayed voting-power read into a larger PTB.
Revenue Sharing
veSCA holders receive protocol revenue in proportion to voting power:
your_share = your_voting_power / total_voting_power
your_revenue = total_protocol_revenue * your_share
Your share falls as others lock more and as your own power decays. Details: revenue-sharing.md.
Claim Revenue
veSCA holders can claim revenue through loyalty and referral programs:
tx = builder.create_tx_block()
# Claim loyalty revenue (SCA rewards)
revenue = tx.claim_loyalty_revenue(vesca_key_id=ve_sca_key_id)
tx.transfer_objects([revenue], wallet_address)
result = builder.sign_and_send_tx_block(tx)
See Loyalty and Referral for more claim options.
Complete veSCA Flow
from sui_scallop_sdk import ScallopClient
client = ScallopClient(secret_key="...", network="mainnet")
builder = client.create_builder()
# Step 1: Lock SCA for 2 years (730 days)
tx1 = builder.create_tx_block()
sca_coin_idx = tx1.add_object_input("0xSCA_COIN_OBJECT_ID")
ve_sca_key = tx1.lock_sca(sca_coin_idx, lock_period_days=730)
tx1.transfer_objects([ve_sca_key], client.wallet_address)
result1 = builder.sign_and_send_tx_block(tx1)
ve_sca_key_id = parse_created_object(result1)
print(f"Locked SCA for 2 years. veSCA key: {ve_sca_key_id}")
# Step 2: Check veSCA object
query = client.create_query()
vesca_data = query.get_object(ve_sca_key_id)
print(f"veSCA created: {ve_sca_key_id}")
# Step 3: Extend lock to 4 years for max voting power
tx2 = builder.create_tx_block()
new_unlock = int(time.time()) + (4 * 365 * 86400) # seconds
tx2.extend_lock_period(ve_sca_key_id, new_unlock)
builder.sign_and_send_tx_block(tx2)
print("Extended lock to 4 years")
# Step 4: Claim loyalty revenue periodically
tx3 = builder.create_tx_block()
revenue = tx3.claim_loyalty_revenue(ve_sca_key_id)
tx3.transfer_objects([revenue], client.wallet_address)
builder.sign_and_send_tx_block(tx3)
print("Claimed loyalty revenue")
# Step 5: After 4 years - redeem SCA
# tx4 = builder.create_tx_block()
# sca = tx4.redeem_sca(ve_sca_key_id)
# tx4.transfer_objects([sca], client.wallet_address)
Max Lock Strategy
For maximum voting power, lock for the full 1461 days:
tx = builder.create_tx_block()
sca_coin_idx = tx.add_object_input("0xSCA_COIN_OBJECT_ID")
ve_sca_key = tx.lock_sca(
sca_coin_idx=sca_coin_idx,
lock_period_days=1461, # Max: 4 years
)
Power decays continuously, so holding at 100% means periodically extending back out to 1461 days — a max lock is not set-and-forget. See vesca-mechanics.md.
Error Handling
| Error | Cause | Solution |
|---|---|---|
LockPeriodTooShort |
Below minimum lock | Use longer lock period |
LockPeriodTooLong |
Above 4 years | Max is 4 years |
LockNotExpired |
Trying to redeem early | Wait for unlock |
ExtendWouldShortenLock |
New time < current | Extend must increase |
References
- veSCA Mechanics - Lock mechanics
- Voting Power - Power calculations
- Revenue Sharing - Revenue distribution