Skip to content

Performance package withdrawal limits - #494

Open
metapileks wants to merge 15 commits into
developfrom
pileks/met-708-price-based-performance-package-vesting-and-distribution
Open

metapileks wants to merge 15 commits into
developfrom
pileks/met-708-price-based-performance-package-vesting-and-distribution

Conversation

@metapileks

@metapileks metapileks commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Unlocking and withdrawing are now separate steps. complete_unlock only marks tranches as unlocked; the recipient pulls what is unlocked either as tokens (withdraw_tokens) or by selling into the DAO's spot pool and receiving the USDC (withdraw_via_sell).

A package may carry a withdrawal policy: a cap on tokens and a cap on quote value per fixed window, a mode saying which of the two routes are open, and an end date after which the caps fall away. Token withdrawals are valued at the higher of the pool's price observation and its reserve price; sells count what the pool actually paid. Limits are set at creation or through the existing two-party change flow, which gains an UnlockTerms change that moves the cliff and replaces the limits in one step. Without an active policy both routes are open and uncapped.

The performance package account grows from 520 to 582 bytes. A permissionless resize migrates live packages, and every instruction refuses an unmigrated package.

Instruction changes

Instruction Change
resize_performance_package New, permissionless and idempotent. Grows the account to 582 bytes with withdrawal_policy = None; the payer covers the extra rent.
all existing instructions Refused with AccountNotMigrated until the package is resized.
initialize_performance_package_with_limits New, same accounts as the plain initializer. Takes { base, limits } and stores a policy anchored at the creation clock. Caps must be non-zero, the end in the future and the window at least one second (InvalidWithdrawalLimits).
complete_unlock Never transfers tokens. Accounts shrink to the package and the oracle.
withdraw_tokens New, recipient-signed. Pays amount from the vault, bounded by vault minus still-locked (InsufficientWithdrawableBalance). Under an active policy the mode must allow tokens and the window's token and quote caps must fit, valued from the Dao at the oracle account.
withdraw_via_sell New, recipient-signed. The package sells amount from its vault into the DAO's spot pool through futarchy's spot_swap, guarded by min_quote_out, and forwards exactly the proceeds. Under an active policy the mode must allow sells, the token cap applies and the quote cap counts the proceeds.
propose_change / execute_change New ChangeType::UnlockTerms { min_unlock_timestamp, limits: Option<LimitsParams> }, allowed in any state. Propose validates the limits; execute sets the cliff and replaces the policy: same window length keeps the usage, a different one re-anchors the windows at now, None removes it.
burn_performance_package Pays the withdrawable balance to the recipient's ATA (created if missing, paid by the admin) before burning the locked remainder, then closes the emptied vault to the spill account. Optionally takes a quote mint, the package's ATA for it and a destination token account: the ATA's balance moves to the destination and the ATA closes. The quote mint must differ from the token mint (InvalidQuoteMint); the ATA and destination come together (QuoteSweepAccountsIncomplete). New accounts: recipient, its ATA, the three optional quote accounts, system and associated token programs.

State, events and errors

  • PerformancePackage.withdrawal_policy: Option<WithdrawalPolicy> appended, with limits: WithdrawalLimits (start, end, window seconds, both caps, mode Tokens | Sell | Both) and usage: WindowUsage (window index, tokens used, quote used). OldPerformancePackage decodes the 520-byte layout for the resize.
  • The program now depends on futarchy with the cpi feature.
  • New events TokensWithdrawn (with capped: Option<CappedWithdrawal>, the valuation price, quote value and usage when limits were active) and TokensSold (amount, proceeds, min_quote_out, capped: Option<WindowUsage>).
  • Errors appended: InvalidWithdrawalLimits, InsufficientWithdrawableBalance, TokenWindowLimitExceeded, QuoteWindowLimitExceeded, InvalidPriceObservation, WithdrawTokensDisabled, WithdrawViaSellDisabled, AccountNotMigrated, InvalidQuoteMint, QuoteSweepAccountsIncomplete.

SDK and scripts

  • price_based_performance_package/v0.6 regenerated in place. completeUnlockIx takes only the package and oracle; new initializePerformancePackageWithLimitsIx, withdrawTokensIx, withdrawViaSellIx, resizePerformancePackageIx; burnPerformancePackageIx takes the recipient and, optionally, quoteMint and quoteDestination for the sweep; proposeChangeIx is typed with ProposeChangeParams.
  • New helper subpath price_based_performance_package/v0.6/withdrawalLimits: active policy, current window and effective usage, withdrawable balance, valuation price, maximum token withdrawal and a sell proceeds estimate, all computed from fetched accounts.
  • Scripts: resizePerformancePackages.ts (modelled on resizeDaos.ts), a proposePerformancePackageUnlockTerms DAO action with the Rip Cars admin enqueue rip-cars/proposeUnlockTerms.ts, executeChange.ts for the recipient, and the burn script passing the new accounts.

Greptile Summary

This PR separates tranche unlocking from withdrawal, adds capped token and sell-based withdrawal routes, migrates existing package accounts to the expanded state layout, and updates retirement, change-management, SDK, scripts, and tests accordingly.

  • Adds withdrawal policies with token and quote caps, fixed windows, route controls, and expiration.
  • Adds permissionless package resizing and migration gates across existing instructions.
  • Adds recipient-driven token withdrawals and spot-pool sales through Futarchy.
  • Updates package retirement to pay unlocked tokens, burn locked tokens, close the vault, and optionally sweep quote proceeds.
  • Refines the SDK quote-sweep interface so callers cannot construct partial sweep arguments.

Confidence Score: 5/5

The PR appears safe to merge; the previously reported SDK argument issue is fixed and no actionable new defect remains.

The earlier migration finding was correctly conceded and its thread is resolved. The quote-sweep builder now accepts a single typed pair, fully addressing the other previous finding. The changes since the prior review introduce no new blocking or non-blocking findings.

Important Files Changed
Filename Overview
programs/price_based_performance_package/src/state/performance_package.rs Expands package state with withdrawal policies, usage accounting, migration sizing, and withdrawable-balance helpers.
programs/price_based_performance_package/src/instructions/withdraw_tokens.rs Adds recipient-authorized token withdrawals with unlocked-balance and active-window cap enforcement.
programs/price_based_performance_package/src/instructions/withdraw_via_sell.rs Adds recipient-authorized spot-pool sales with slippage protection and proceeds-based quote accounting.
programs/price_based_performance_package/src/instructions/burn_performance_package.rs Pays unlocked tokens, burns the locked remainder, closes the vault, and optionally sweeps the package quote account.
programs/price_based_performance_package/src/instructions/resize_performance_package.rs Adds an idempotent, permissionless migration from the legacy package account layout.
sdk/src/price_based_performance_package/v0.6/PriceBasedPerformancePackageClient.ts Updates instruction builders for the expanded program and models burn quote sweeping as one all-or-nothing argument.
tests/priceBasedPerformancePackage/unit/burnPerformancePackage.test.ts Updates burn coverage for the paired quote-sweep SDK interface while retaining direct validation of malformed on-chain accounts.

Reviews (2): Last reviewed commit: "burn pp - move quote sweep into a single..." | Re-trigger Greptile

Context used:

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Repository Guard

  • Cargo.lock: pass
  • yarn.lock (sdk): pass
  • yarn.lock (root): pass
  • Repo guard: pass

Repository Guard

Cargo dependency pinning

  • Status: pass
  • Every programs/*/Cargo.toml dep uses =x.y.z, a path = .. workspace ref, or a git dep with a 40-char rev.

Cross-program Anchor/Solana version consistency

  • Status: pass
  • anchor-lang and anchor-spl are pinned to the version declared in repo-guard.toml across every program.

solana-program crate pin

  • Status: pass
  • Every solana-program = "=X" declaration is =1.17.14 (locked to match Cargo.lock).

Anchor.toml solana_version

  • Status: pass
  • Anchor.toml declares solana_version = "1.17.34" (local-dev install for anchor test).

Crate minimum age

  • Status: pass
  • All Cargo deps changed by this PR are at least 14 days old on crates.io.

Yarn package.json pinning

  • Status: pass
  • All package.json deps use exact versions (no ^, ~, ranges).

npm minimum age

  • Status: pass
  • All npm deps changed by this PR are at least 14 days old.

Workflow toolchain consistency

  • Status: pass
  • Every workflow declares anchor-version: 0.29.0.
  • Per-file solana-cli-version values match [toolchain.workflow_solana_cli] in repo-guard.toml.

GitHub Action SHA pinning

  • Status: pass
  • Every third-party action is pinned to a SHA in [actions.sha_allowlist].

Sensitive program / config changes

  • Status: warn
  • Review hint only (CODEOWNERS is the merge gate). Lines below match heuristics for security-sensitive changes:
  • High-sensitivity files touched: Cargo.lock
  • scripts/v0.7/rip-cars/proposeUnlockTerms.ts:27 Hardcoded Solana address literal -> + "92NY2WWWNAfnMr8qyGXWXKbauhnR7d6ei9j9BGHVwh7r",
  • sdk/src/price_based_performance_package/v0.6/types/price_based_performance_package.ts:848 Hardcoded Solana address literal -> + name: "ChangePerformancePackageAuthorityParams";
  • sdk/src/price_based_performance_package/v0.6/types/price_based_performance_package.ts:1570 Hardcoded Solana address literal -> + name: "PerformancePackageAuthorityChanged";
  • sdk/src/price_based_performance_package/v0.6/types/price_based_performance_package.ts:2051 Hardcoded Solana address literal -> + name: "changePerformancePackageAuthority",
  • sdk/src/price_based_performance_package/v0.6/types/price_based_performance_package.ts:2078 Hardcoded Solana address literal -> + defined: "ChangePerformancePackageAuthorityParams",
  • sdk/src/price_based_performance_package/v0.6/types/price_based_performance_package.ts:2583 Hardcoded Solana address literal -> + name: "ChangePerformancePackageAuthorityParams",

Overall status: pass

Lockfile freshness (Cargo.lock + yarn.lock) is checked by the workflow directly and cannot be bypassed. The sensitive-diff section is a review hint - CODEOWNERS handles the actual merge gate.

@metapileks
metapileks marked this pull request as ready for review September 15, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant