This implementation adds key staking functionality to the creator-keys contract, ensuring that staked keys cannot be sold until they are explicitly unstaked by the holder.
- Added
StakedBalance(Address, Address)toDataKeyenum: Tracks staked amount per (creator, holder) pair - Added
staked_balance()helper function: Returns storage key for staked balance lookup
- Stakes a specified amount of keys for a holder
- Requires holder authorization
- Validates that holder has sufficient liquid balance before staking
- Increments the staked balance
- Errors:
NotPositiveAmountif amount is zeroInsufficientBalanceif liquid balance < amountProtocolPausedif contract is paused
- Unstakes a specified amount of previously staked keys
- Requires holder authorization
- Decrements the staked balance
- Removes storage entry when staked balance reaches zero
- Errors:
NotPositiveAmountif amount is zeroInsufficientBalanceif staked balance < amountProtocolPausedif contract is paused
- Read-only view function
- Returns the number of staked keys for a holder
- Returns 0 if no keys are staked
- Read-only view function
- Returns sellable balance (total balance - staked balance)
- Returns 0 if all keys are staked or holder has no keys
- Modified to check liquid balance instead of just total balance
- Calculates liquid balance as:
total_balance - staked_balance - Rejects sell attempts if liquid balance is zero
- Key Change: Added staked balance check before processing sell
// Check liquid balance (total balance - staked balance)
let staked_balance_key = constants::storage::staked_balance(&creator, &seller);
let staked_balance: u32 = env.storage().persistent().get(&staked_balance_key).unwrap_or(0);
let liquid_balance = current_balance.saturating_sub(staked_balance);
if liquid_balance == 0 {
return Err(ContractError::InsufficientBalance);
}- Setup: Holder has 10 keys, stakes 6 (leaving 4 liquid)
- Action: Attempt to sell 5 keys
- Expected: Reverts with
InsufficientBalanceerror - Verifies: Staked keys cannot be accessed for selling
- Setup: Holder has 10 keys, stakes 6 (leaving 4 liquid)
- Action: Sell exactly 4 keys (one at a time)
- Expected: All 4 sells succeed
- Verifies:
- Liquid balance reaches 0
- Staked balance unchanged at 6
- Total balance is 6 (all staked)
- Setup: Holder has 10 keys, stakes 6
- Action:
- Attempt to sell 5 keys (fails)
- Successfully sell 4 keys
- Expected: Staked balance remains at 6 throughout
- Verifies: Staked balance is immutable through sell operations
✅ Sell of 5 reverts when only 4 liquid keys available
- Implemented in
test_sell_reverts_when_attempting_to_use_staked_keys - When 10 total keys with 6 staked (4 liquid), selling 5 returns
InsufficientBalance
✅ Sell of 4 succeeds using only liquid balance
- Implemented in
test_sell_succeeds_within_liquid_balance_limit - All 4 liquid keys can be sold individually
- Staked keys remain untouched
✅ Staked balance unchanged after both attempts
- Verified in both test cases
- Failed sell attempt doesn't affect staked balance
- Successful sells only reduce liquid balance
- Staked balance remains constant at 6
- Staked balance is stored separately from total balance
- Uses sparse storage (only stores non-zero values)
- Storage key:
DataKey::StakedBalance(creator.clone(), holder.clone())
- Total Balance: Stored in
KeyBalance(creator, holder) - Staked Balance: Stored in
StakedBalance(creator, holder) - Liquid Balance: Calculated as
total - staked(usessaturating_subfor safety)
- Reuses existing
ContractErrorvariants:NotPositiveAmount: For zero amount operationsInsufficientBalance: For insufficient liquid/staked balanceProtocolPaused: For operations during pauseOverflow: For arithmetic overflow protection
- Both
stake_keysandunstake_keysrequire holder authorization - Uses
holder.require_auth()to ensure only the holder can stake/unstake their keys
feat: implement key staking to prevent selling of staked keys
- Add StakedBalance data key to track staked keys per (creator, holder)
- Add staked_balance storage helper function
- Implement stake_keys() to lock keys from being sold
- Implement unstake_keys() to unlock previously staked keys
- Implement get_staked_balance() to query staked amount
- Implement get_liquid_balance() to query sellable amount
- Modify sell_key() to check liquid balance (total - staked) instead of just total balance
test: add tests for staked keys sell protection
- Test that selling 5 keys fails when only 4 liquid keys available (6 staked out of 10 total)
- Test that selling exactly 4 liquid keys succeeds
- Test that staked balance remains unchanged after failed and successful sell attempts
- Verify InsufficientBalance error when attempting to sell more than liquid balance
- Code follows existing patterns from the codebase
- Uses consistent error handling with other functions
- Follows Rust and Soroban SDK best practices
- Test structure matches existing test patterns
To verify this implementation:
- Install MSVC Build Tools or Visual Studio with C++ support
- Run
cargo test --test sell_requires_liquid_balance - Run
cargo testto ensure no regressions in existing tests - Review contract size and gas costs if needed
- No reentrancy risks: All state changes happen atomically
- Overflow protection: Uses checked arithmetic operations
- Authorization: Requires holder auth for stake/unstake operations
- Sparse storage: Only stores non-zero staked balances to save space
- Backward compatible: Existing functionality unaffected (zero staked balance = all keys liquid)