Skip to content

farming-pool: lock_assets/unlock_assets/stake/set_boost validate amounts with untyped assert! instead of typed PoolError variants #66

Description

@prodbycorne

Problem

Amount and allocation validation across the fund-handling entry points uses untyped assert! panics instead of the typed PoolError pattern the contract otherwise uses consistently:

pub fn lock_assets(env: Env, user: Address, amount: i128) -> Result<(), PoolError> {
    // ...
    assert!(amount > 0, "amount must be positive");
    // ...
}

pub fn unlock_assets(env: Env, user: Address, amount: i128) -> Result<(), PoolError> {
    // ...
    assert!(amount > 0, "amount must be positive");
    // ...
    assert!(amount <= position.amount, "insufficient locked balance");
    // ...
    assert!(current >= position.unlock_ledger, "minimum lock period not elapsed");
}

pub fn stake(env: Env, from: Address, amount: i128) -> Result<(), PoolError> {
    // ...
    assert!(amount > 0, "amount must be positive");
}

pub fn set_boost(env: Env, user: Address, allocation_pct: u32) -> Result<(), PoolError> {
    // ...
    assert!(allocation_pct >= 1 && allocation_pct <= 100, "allocation_pct must be 1-100");
}

Every one of these functions already declares Result<(), PoolError> as its return type — the plumbing for typed errors is already in place — yet none of these six validation checks use it. set_credit_rate (PoolError::InvalidCreditRate) is the only place in the whole contract that validates fund/rate parameters via a typed error instead of a panic, making the contract internally inconsistent about how it signals invalid input.

Impact

  • Poor integrator experience: a frontend calling try_lock_assets(&user, &-5) or try_unlock_assets(&user, &(position.amount + 1)) gets an opaque host trap it cannot pattern-match on, instead of a typed error like the rest of the API promises.
  • Inconsistent contract surface: reviewers and integrators cannot rely on PoolError as the single source of truth for "what can go wrong" — some failures are typed, others are untyped panics, for what are conceptually the same class of validation failure (bad caller input).

Fix

Add dedicated typed variants and replace each assert! with a Result-returning check:

pub enum PoolError {
    // ...
    InvalidAmount = 16,
    InsufficientBalance = 17,
    LockPeriodNotElapsed = 18,
    InvalidAllocation = 19,
}

// lock_assets / stake:
if amount <= 0 { return Err(PoolError::InvalidAmount); }

// unlock_assets:
if amount <= 0 { return Err(PoolError::InvalidAmount); }
if amount > position.amount { return Err(PoolError::InsufficientBalance); }
if current < position.unlock_ledger { return Err(PoolError::LockPeriodNotElapsed); }

// set_boost:
if !(1..=100).contains(&allocation_pct) { return Err(PoolError::InvalidAllocation); }

Acceptance Criteria

  • New PoolError variants added for amount/allocation/lock-period validation (exact names/numbering at maintainer discretion, but must not collide with existing variants).
  • lock_assets, unlock_assets, stake, set_boost return typed Err(PoolError::_) instead of panicking via assert! for all input-validation failures.
  • Existing tests using try_lock_assets/try_unlock_assets/try_stake/try_set_boost and asserting .is_err() continue to pass, updated to assert the specific PoolError variant where practical (e.g. test_set_boost_rejects_zero_allocation, test_lock_assets_rejects_negative_amount).
  • No assert! remains for caller-input validation in lock_assets, unlock_assets, stake, set_boost.

Additional Notes

Confirmed against current source: lock_assets (farming-pool/src/lib.rs:225-263) asserts amount > 0 at line 229; unlock_assets (265-303) asserts amount > 0 (269), amount <= position.amount (273), and current >= position.unlock_ledger (276-279); stake (404-436) asserts amount > 0 (408); set_boost (459-484) asserts allocation_pct in 1..=100 (463-466). Confirmed set_credit_rate (513-530) is the only validation in the contract using a typed error (PoolError::InvalidCreditRate, line 517) instead of assert!.

Edge cases:

Implementation sketch: add PoolError::InvalidAmount, InsufficientBalance, LockPeriodNotElapsed, InvalidAllocation (types.rs, after NoActiveStake = 14, e.g. 16-19, leaving 15 free for #62's CreditOverflow); replace each assert! at the six call sites above with the corresponding if ... { return Err(...); } exactly as the issue's fix snippet shows.

Test plan: update test_lock_assets_rejects_zero_amount, test_lock_assets_rejects_negative_amount, test_unlock_assets_rejects_zero_amount, test_unlock_assets_rejects_more_than_locked, test_set_boost_rejects_zero_allocation, test_set_boost_rejects_over_100_allocation (all confirmed present in farming-pool/src/test.rs) to assert the specific PoolError variant via try_* rather than just .is_err()/should_panic. Add a new test for the "multiple constraints violated simultaneously" ordering case in unlock_assets noted above (e.g. test_unlock_assets_zero_amount_and_no_position_reports_amount_error_first or the reverse, whichever the preserved order dictates).

Cross-references: #64 (same theme, admin-getter functions), #65 (adjacent lines in unlock_assets/unstake — coordinate), #67 (same theme, initialize/set_global_multiplier), #68 (same theme, pause guard; also same enum-discriminant coordination concern), #62 (discriminant-numbering coordination for CreditOverflow).

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignOfficial Campaign | FWC26Campaign: Official Campaign | FWC26correctnessLogic correctness and invariant enforcementfarming-poolFarmingPool contractvery hardExtremely hard — deep expertise, careful design, and significant time required

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions