The SmartDrop FarmingPool smart contract provides two staking models tailored to different campaign requirements:
- Lock/Unlock Position System (
Position): Time-locked staking where deposits are committed for a minimum duration (min_lock_period). - Boost / Stake System (
UserStake): Continuous, non-locked staking with optional user-allocated multiplier boosts (BoostConfig).
Both systems can coexist within the same deployed pool instance, and users may participate in either or both concurrently.
| Feature | Time-Locked System (Position) |
Boost / Stake System (UserStake) |
|---|---|---|
| Primary State Struct | Position |
UserStake |
| Storage Key | DataKey::UserPosition(Address) |
DataKey::UserStake(Address) |
| Deposit Function | lock_assets(user, amount) |
stake(from, amount) |
| Withdraw Function | unlock_assets(user, amount) |
unstake(from) |
| Time Lock | Enforces min_lock_period ledgers |
None (flexible withdrawal at any time) |
| Partial Withdrawal | Supported (amount <= locked amount) | Full unstake of current balance |
| Boost Multipliers | Fixed standard accrual | Configurable boost via set_boost(allocation_pct) |
| Credit Rate Snapshot | Checkpointed in checkpoint_ledger |
Checkpointed in start_ledger |
| Credit Calculation | calculate_credits(user) / get_position_credits(user) |
get_stake_credits(user) (get_credits(user) for combined total) |
| Emitted Events | (pool, locked), (pool, unlocked) |
None (governed via token/boost events) |
Accrued credits for locked positions grow linearly with the elapsed ledgers and pool credit rate:
Where
Accrued credits incorporate the user's boost allocation percentage and global multiplier:
Where
When unexpected market conditions, contract upgrades, or security events require pausing a pool (pause()), normal deposits and withdrawals (lock_assets, unlock_assets, stake, unstake) are halted to protect contract invariants.
- Trigger Conditions:
emergency_withdraw(user)can only be executed while the pool is paused (pool_is_paused). - User Self-Withdrawal: Users can call
emergency_withdraw(user)directly while the pool is paused, requiring caller authorization (user.require_auth()). - Atomic Exit: All locked tokens in
Positionand staked tokens inUserStakeare transferred back to the user in a single atomic transaction. - Accrual History Preservation: Accrued credits are preserved in
BankedCreditsunderBankedCreditTotals { position_credits, stake_credits }, allowing users and indexers to inspect credits earned prior to the emergency exit viaget_banked_credits_split(user)andget_banked_credits(user). - User Notification: Every call emits an on-chain
(symbol_short!("pool"), symbol_short!("emrg_exit"))event with payload(admin, user, total_returned). Off-chain indexers monitor this event topic to alert affected users. - Audit Requirements & Privilege Controls: Because emergency operations allow user capital exits during security pauses, all admin calls toggling pause state or executing emergency workflows MUST produce immutable event logs and be controlled via multi-signature accounts or time-locked governance contracts.
The flexible UserStake system deliberately does not enforce a lock period like the Position system does (stake/unstake vs lock_assets/unlock_assets). This is a documented design decision, not an oversight (#169):
- Different product purpose: the lock system commits deposits for a minimum duration; the boost/stake system exists for continuous flexible staking where a lock would defeat its purpose.
- No leverage, no flash-staking reward: a stake is not a loan —
unstakereturns only the exact staked principal. Credits accrue linearly over elapsed ledgers (compute_stake_accrual), with checkpoints on bothstakeandunstake, so an immediate stake→unstake round-trip banks ~0 credits. There is no fixed up-front reward to harvest. - Bounded exposure: the maximum "harm" of free stake/unstake is per-transaction gas, which the caller pays. The contract never over-commits liabilities beyond staked amounts.
- Guidance: pools that need a commitment lock should rely on the
Positionsystem; pools that want flexible boosted staking use the stake system as-is. If a future product needs a locked boosted stake, add a separate opt-inmin_stake_lock_periodparameter rather than coupling the two model.
The Factory contract manages pool deployments and provides query functions to locate pools by staking asset:
get_pools_by_asset(asset, start_id, limit): Scans up toMAX_POOL_SCAN_PER_CALL(200) pool IDs per invocation.get_pools_by_asset_range(asset, start_id, scan_limit, limit): Allows callers to specify a custom scan windowscan_limit(capped at 200).
- Deterministic Resource Consumption: Bounding the scan window per call prevents transactions from exceeding Soroban's CPU instruction (100M) and persistent storage read entry footprint budgets.
- Pagination: Callers can iterate by passing
next_start_idasstart_iduntilnext_start_id == total. - Off-Chain Indexer Best Practice: For high-volume production frontends querying across thousands of registered pools, frontends should subscribe to and index
(symbol_short!("factory"), symbol_short!("pool_crtd"))events rather than scanning on-chain.