feat(farming-pool): support partial withdrawal in unstake (closes #77) - #140
Open
Cyber-Mitch wants to merge 1 commit into
Open
feat(farming-pool): support partial withdrawal in unstake (closes #77)#140Cyber-Mitch wants to merge 1 commit into
Cyber-Mitch wants to merge 1 commit into
Conversation
✅ Deploy Preview for sdcontracts ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #77
unstake()always withdrew the entire staked balance — no way to withdraw part of a position while keeping the rest earning credits, unlikeunlock_assets(), which already supports partial withdrawal for the lock system. This adds anamountparameter tounstake, mirroringunlock_assets's pattern.Before:
unstake(from: Address) -> Result<i128, PoolError>— always withdrew the entire stake.After:
unstake(from: Address, amount: i128) -> Result<i128, PoolError>— withdraws exactlyamount.amountis a new required third parameter. Existing two-argument callers fail at the VM boundary withFunc(MismatchingParameterLen)/Error(WasmVm, UnexpectedSize)— a hard invocation failure, not a silent misread.Migration — to preserve previous behavior, read the stake first and pass its full amount:
New error variant:
PoolError::InvalidAmount = 10, returned whenamount <= 0oramount > stake.amount. Existing discriminants 1–9 are unchanged — no other error code shifts.Behavior change on the no-stake path: calling
unstakewith no stake record previously panicked (expect("no active stake")); it now returns the typedPoolError::NoActiveStake = 8, matchingemergency_withdraw. Callers relying on a trap must handle a typed error instead.Unchanged: the return value is still the caller's full banked credit balance after checkpointing — it is not prorated to
amount. Boost allocations (DataKey::UserBoost) survive partial withdrawals and continue applying to the remainder.Downstream fixture note:
factory/tests/fixtures/farming_pool.wasmis a checked-in build of this contract and has been regenerated. Any other repo vendoring farming-pool WASM must re-vendor it.Recon found three drifts from the issue's snapshot
PoolErrorhas a gap between discriminants 3 and 13MinLockPeriodOutOfRangeis a farming-pool variant from #89FactoryError(factory/src/types.rs:79)#89has landed on this base (InvalidCreditRate=3,InvalidGlobalMultiplier=4,NotInitialized=2).#75's property tests have not landed — that work lives on an unmerged branch; extending its generator with partial-unstake ops (as the issue suggests as a follow-up) isn't possible on this base and wasn't attempted.Also found:
unstake()used.expect("no active stake")— an untyped panic — despitePoolError::NoActiveStake = 8already existing and already used byemergency_withdraw. Fixed as part of this change (see the breaking-change notice above).Design decisions
InvalidAmount = 10— the enum is contiguous 1..=9, so 10 is the next free value. Nothing in the repo uses a discriminant ≥ 10.full_unstakewrapper.unlock_assets— the function this issue says to mirror — has supported partial withdrawal since inception with nofull_unlockcompanion; a wrapper would makeunstakeless consistent with its own template. It also can't preserve real backward compatibility anyway: deployed clients call by WASM ABI, and a 2-arg call breaks regardless of whether a 3-arg convenience function exists alongside it. Confirmed no script, Makefile, or doc in the repo callsunstakein a way a wrapper would help.assert!.unlock_assets(the template) validates withassert!;unstakenow uses typedPoolErrorreturns instead. Deliberate divergence, confirmed before implementation — harmonizingunlock_assetsonto typed errors would be a natural but separate follow-up.UserBoost orphaning — verified by reading, then by mutation
checkpoint()readsget_user_boostfresh on every call (lib.rs:265-266) andUserStakecaches no boost field, so a partial unstake leaving a remainder cannot leave stale boost data. Not left at inspection: I mutation-tested it — injecting aUserBoostremoval into the partial-withdrawal branch madetest_unstake_partial_preserves_boost_allocationfail on the credited amount exactly as predicted (21000vs expected24000). A second mutation (checkpoint moved after the balance subtraction) was independently caught by 4 tests.Tests (118 passing, +7 new; 191 total across the workspace)
test_unstake_partial_keeps_remaining_stake— mirrorstest_unlock_assets_partial_keeps_remaining_position; asserts continued accrual after partial withdrawal.test_unstake_partial_preserves_boost_allocation— mutation-verified per above.amount == stake.amount(identical to full unstake,get_stakereturnsNoneafterward),amount == stake.amount + 1(rejected),amount == 0(rejected), negative amount (rejected).NoActiveStaketyped-error test for the no-stake path.test.rs:1855whose arity would otherwise have silently mismatched — itsreentry_was_rejected()check would have returnedtruefor the wrong reason (an arity failure, not a rejected re-entrant call), testing nothing.Verification
cargo build --workspace --target wasm32v1-none --releasecargo test --workspacecargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningsci.yml:41doesn't use--all-features)Changed files
factory/tests/factory_pool_integration.rs | 4 +-
factory/tests/fixtures/farming_pool.wasm | Bin 41540 -> 43198
farming-pool/src/lib.rs | 64 ++++++-
farming-pool/src/test.rs | 150 +++++++++++++-
farming-pool/src/types.rs | 3 +
Two files beyond
lib.rs/types.rs/test.rswere required, not scope creep: the integration crate has its own call sites (missed by the issue), and the checked-in WASM fixture had to be regenerated since the integration test deploys from it viainclude_bytes!rather than a fresh build — leaving it stale fails withMismatchingParameterLen.Snapshot files intentionally excluded. Running the suite regenerates
test_snapshots/*.json— confirmed via a pristinegit archiveexport that 37 tracked snapshots already drift atHEADwith zero source changes, a pre-existing repo hygiene issue this PR doesn't touch. Excluded from this commit to keep the diff to the funds-custody logic actually being changed.Acceptance criteria
unstakeaccepts anamountparameter and supports partial withdrawal, remainder still earning creditsunstakerejectsamount <= 0oramount > stake.amountwith a typed errortest_unstake_partial_keeps_remaining_stakepresent, mirroringtest_unlock_assets_partial_keeps_remaining_positiontest_unstake_returns_tokens_and_credits,test_pause_blocks_unstake,test_unpause_restores_unstakeupdated for the new signature (plus 3 more sites the issue didn't name)Found, not fixed
unstaked/stakedevents.docs/events.mdspecifies anunstakedevent with(user, amount, total_credits)and astakedevent, but neitherunstake()norstake()emits anything — onlylocked/unlocked/emrg_exit/adm_xfr/upgradedexist. Pre-existing drift, not introduced here. Worth its own issue — the payloaddocs/events.mdspecifies is now finally expressible withamountavailable.unlock_assetsstill validates withassert!whileunstakenow uses typed errors (deliberate, per the design decision above) — flagging the inconsistency for awareness, not proposing to fix it here since harmonizing would be its own breaking change.