Staking Pools (Interest-Bearing Token Pools)
Overview
BiatecCLAMM now supports staking pools where asset A and asset B are the same token. This enables the creation of interest-bearing tokens like B-ALGO, B-USDC, etc., where liquidity providers can earn rewards from staking rewards, transaction fees, or other revenue sources that accrue to the pool.
Use Cases
1. Native Token Staking (B-ALGO)
Create a pool where both asset A and asset B are set to 0 (native token). This creates a B-ALGO token that represents staked ALGO. Any ALGO that accrues to the pool (e.g., from consensus rewards, governance rewards, or direct deposits) can be distributed to B-ALGO holders.
2. Asset Staking (B-USDC, B-TOKEN, etc.)
Create a pool where both asset A and asset B are set to the same ASA ID. This creates a B-{TOKEN} token that represents staked tokens. This is useful for:
- Lending protocols where deposited assets earn interest
- Revenue-sharing mechanisms
- Yield aggregation strategies
Pool Characteristics
When asset A equals asset B:
- LP Token Name:
B-{AssetName}(e.g., "B-ALGO", "B-USDC") - LP Token Symbol: The asset's unit name (e.g., "ALGO", "USDC")
- Price Range: Must be flat at 1:1 (priceMin = priceMax = currentPrice = SCALE). The contract now enforces this during bootstrap (error
E_STAKING_PRICE). - Swaps: Not meaningful since both assets are the same
- Main Operations: Add liquidity (stake), remove liquidity (unstake), distribute rewards
How It Works
1. Pool Creation
const { clientBiatecClammPoolProvider } = await setupPool({
algod,
assetA: 0n, // 0 for ALGO, or an ASA ID
assetB: 0n, // Same as assetA
biatecFee: 0n, // No Biatec fee
lpFee: BigInt(SCALE / 100), // 1% fee (optional)
p: BigInt(1 * SCALE), // Price = 1:1
p1: BigInt(1 * SCALE), // Min price = 1
p2: BigInt(1 * SCALE), // Max price = 1
nativeTokenName: 'Algo', // Optional helper parameter ensures provider global state matches
});
// When constructing transactions manually, configure the provider once via:
// await poolProviderClient.send.setNativeTokenName({
// args: { appBiatecConfigProvider: configAppId, nativeTokenName: 'Algo' },
// appReferences: [configAppId],
// });
2. Adding Liquidity (Staking)
Users add liquidity to stake their tokens:
const txId = await clammAddLiquiditySender({
algod,
account: userSigner,
amountA: stakeAmount, // Amount to stake
amountB: stakeAmount, // Same as amountA
assetA: 0n, // 0 for ALGO
assetB: 0n, // Same as assetA
assetLP: lpTokenId,
clientBiatecClammPool,
appBiatecConfigProvider,
appBiatecIdentityProvider,
appBiatecPoolProvider,
});
Users receive B-{TOKEN} LP tokens representing their staked position.
3. Distributing Rewards
When rewards accrue to the pool (e.g., staking rewards, fees, direct deposits), the executive fee address distributes them:
// 1. Rewards are deposited to the pool address
// (This can happen automatically via consensus rewards, or manually)
// 2. Executive fee address distributes the excess assets
const txId = await clammDistributeExcessAssetsSender({
algod,
account: executiveSigner,
amountA: rewardsAmount * (SCALE / assetDecimals), // In base scale (9 decimals)
amountB: 0n, // No rewards for asset B
assetA: 0n, // 0 for ALGO
assetB: 0n, // Same as assetA
clientBiatecClammPool,
appBiatecConfigProvider,
});
This increases the pool's liquidity proportionally, so when users withdraw, they receive more tokens than they deposited.
4. Removing Liquidity (Unstaking)
Users withdraw their stake plus earned rewards:
const txId = await clammRemoveLiquiditySender({
algod,
account: userSigner,
assetA: 0n,
assetB: 0n,
assetLP: lpTokenId,
lpTokensToSend: lpBalance, // All or partial LP tokens
clientBiatecClammPool,
appBiatecConfigProvider,
appBiatecIdentityProvider,
appBiatecPoolProvider,
});
Users receive back their staked tokens plus a proportional share of the rewards.
Technical Details
Contract Changes
-
Pool Provider Global State: Added
nativeTokenName(nt) to the pool provider global state with an admin-onlysetNativeTokenNamemethod. The CLAMM bootstrap reads this value when creating LP tokens. -
Asset Validation: Removed the assertion that prevented
assetA.id === assetB.id. The contract now supports this configuration for staking pools. -
LP Token Naming:
- Standard pools:
B-{AssetA}-{AssetB}with unit nameBLP - Staking pools:
B-{AssetName}with unit name matching the underlying asset
- Standard pools:
-
Opt-in Logic: Skips duplicate opt-in when assetA equals assetB.
TypeScript API Changes
-
clammCreateTxs / clammCreateSender: No longer accept a
nativeTokenNameparameter; configure the pool provider once viaBiatecPoolProviderClient.send.setNativeTokenName. -
setupPool: Added optional
assetBandnativeTokenNameparameters for test scenarios
Examples
Example 1: B-ALGO Pool
// Create B-ALGO pool
const pool = await setupPool({
algod,
assetA: 0n,
assetB: 0n,
biatecFee: 0n,
lpFee: 0n,
p: BigInt(SCALE),
p1: BigInt(SCALE),
p2: BigInt(SCALE),
nativeTokenName: 'Algo',
});
// Stake 100 ALGO
await clammAddLiquiditySender({
amountA: 100n * BigInt(SCALE_ALGO),
amountB: 100n * BigInt(SCALE_ALGO),
// ... other params
});
// Simulate 10 ALGO rewards accruing to pool
// (Sent directly to pool address)
// Distribute rewards
await clammDistributeExcessAssetsSender({
amountA: 10n * BigInt(SCALE), // 10 ALGO in base scale
amountB: 0n,
// ... other params
});
// Unstake (receives original 100 ALGO + proportional rewards)
await clammRemoveLiquiditySender({
lpTokensToSend: lpBalance,
// ... other params
});
Example 2: B-USDC Pool (Lending Protocol)
// Create B-USDC pool
const pool = await setupPool({
algod,
assetA: USDC_ASSET_ID,
assetB: USDC_ASSET_ID,
biatecFee: 0n,
lpFee: 0n,
p: BigInt(SCALE),
p1: BigInt(SCALE),
p2: BigInt(SCALE),
});
// User deposits 1000 USDC
await clammAddLiquiditySender({
amountA: 1000n * BigInt(USDC_DECIMALS),
amountB: 1000n * BigInt(USDC_DECIMALS),
// ... other params
});
// Over time, lending interest accrues to the pool
// Executive address distributes interest to LP holders
await clammDistributeExcessAssetsSender({
amountA: interestAmount,
amountB: 0n,
// ... other params
});
// User withdraws their deposit + earned interest
await clammRemoveLiquiditySender({
lpTokensToSend: lpBalance,
// ... other params
});
Security Considerations
Trust Model
Staking pools (B-ALGO, B-USDC, etc.) require trust in specific addresses and processes:
-
Executive Fee Address Control: Only the
addressExecutiveFeeaccount configured in the Biatec Config Provider can distribute rewards viadistributeExcessAssets. This address has significant power:- Can distribute rewards to all LP holders
- Can influence timing of reward distribution
- Must accurately calculate reward amounts
- Recommendation: Use a multi-signature account for this address
-
Config Provider Integrity: The config provider contract controls critical parameters:
- Identity provider reference
- Fee structure
- Executive addresses
- Recommendation: Ensure config provider is immutable or governed by DAO
Differences from Liquidity Pools
Staking pools have unique characteristics:
- No Impermanent Loss: Since both assets are identical, there's no price risk
- No Swap Price Discovery: Rewards come from external sources, not trades
- Simpler Price Model: Always 1:1 at base scale
- Reward Rate Externally Set: Returns depend on reward distribution by executive address
- Swap Operations Blocked: The contract explicitly prevents swaps with error "Swaps not allowed in staking pools"
Risk Factors
-
Reward Shortfall: If rewards aren't distributed as expected, staking yields nothing
- Mitigation: Monitor executive address activity and reward distribution schedule
-
Accounting Errors: Incorrect
distributeExcessAssetscalls could lock funds or distribute unfairly- Mitigation: Thoroughly test reward distribution calculations off-chain first
- Mitigation: Use the base scale (9 decimals) for all calculations
-
Governance Changes: Executive fee address change affects control
- Mitigation: Require timelock for address changes
- Mitigation: Multi-sig governance for config updates
-
Price Validation: Staking pools require
priceMin === priceMax === currentPrice- The contract now enforces this validation in the
bootstrapfunction - Error code:
E_STAKING_PRICEif price range is not flat
- The contract now enforces this validation in the
-
Asset Order Validation: Standard pools now enforce
assetA.id < assetB.idfor non-staking pools- Staking pools bypass this check when
assetA.id === assetB.id - Error code:
E_ASSET_ORDERif order is wrong in standard pools
- Staking pools bypass this check when
Reward Distribution Best Practices
- Calculate in Base Scale: Always convert reward amounts to base scale (multiply by 1e9) before calling
distributeExcessAssets - Verify Pool Balance: Ensure the pool actually received the reward tokens before distributing
- Test First: Run reward distribution on testnet with small amounts first
- Document Schedule: Clearly communicate reward distribution frequency and amounts
- Monitor State: Track pool liquidity before and after distribution to verify correctness
Example Safe Reward Distribution
// 1. Calculate reward in base scale
const rewardInTokenUnits = 1000n; // 1000 tokens
const tokenDecimals = 6n; // USDC has 6 decimals
const baseScale = 1_000_000_000n;
const rewardInBaseScale = rewardInTokenUnits * (baseScale / 10n ** tokenDecimals);
// 2. Send tokens to pool first
await algod.sendPaymentTransaction({
from: executiveAddress,
to: poolAddress,
amount: rewardInTokenUnits * 10n ** tokenDecimals, // In native units
});
// 3. Distribute via contract
await clammDistributeExcessAssetsSender({
appBiatecClammPool: poolAppId,
appBiatecConfigProvider: configAppId,
assetA: usdcAssetId,
assetB: usdcAssetId,
amountA: rewardInBaseScale,
amountB: 0n,
// ... other params
});
Security Audits
Multiple AI-powered security audits have been conducted on the staking pool implementation. Key findings addressed:
- [M-02] Added validation for same-asset pool price ranges (must be flat)
- [M-06] Recommend adding deposit proof to
distributeExcessAssetsfor safety - General Staking pools are now explicitly validated during bootstrap
See audits/ folder for detailed security audit reports.
User Protection
-
Reward Calculation: The
distributeExcessAssetsmethod increases the pool's liquidity proportionally. Ensure theamountAparameter is calculated correctly in base scale (9 decimals). -
Price Stability: Since staking pools use a 1:1 price ratio, the price should remain constant. Any significant deviation may indicate an issue.
-
Rounding: As with standard pools, small amounts may be lost to rounding. This is by design to protect the pool from bleeding.
Testing
See __test__/pool/staking.test.ts for comprehensive test examples including:
- Creating a B-ALGO pool
- Creating a B-TOKEN pool with an ASA
- Distributing rewards and verifying LP profit
Chain-Specific Configuration
Different blockchain networks may use different native token names:
- Algorand Mainnet/Testnet: 'ALGO'
- Voi Network: 'VOI'
- Aramid Network: 'ARAMID'
Use setNativeTokenName to configure the provider before deploying pools to ensure the LP token naming matches the target chain.