Skip to content

Feat/sunset v2#76

Open
galacticminter wants to merge 55 commits into
stader-labs:mainfrom
blockgroot:feat/sunset-v2
Open

Feat/sunset v2#76
galacticminter wants to merge 55 commits into
stader-labs:mainfrom
blockgroot:feat/sunset-v2

Conversation

@galacticminter

Copy link
Copy Markdown
Contributor

No description provided.

blockgroot and others added 18 commits May 12, 2026 10:46
`error.annotate()` dumps full env (incl. OWNER1_KEY, SAFE_API_KEY) into
the error message. Surfaced repeatedly via pre-commit hook output.
Replace with key-only error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
L1 MaticX MATIC balance = 0. Legacy MATIC paths (deprecated submit,
stakeRewardsAndDistributeFeesMatic) won't materially trigger pre-pause.
Any MATIC dust falls through to sweepToCustody after CUSTODY_DELAY.

Removes IPolygonMigration import, POLYGON_MIGRATION constant, and the
migrate() block. Lean contract, one less trust surface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol
Comment thread contracts/MaticX.sol
Comment thread contracts/MaticX.sol
Comment thread contracts/MaticX.sol
Replace the finalizeTerminalRate require on sweepToCustodyTimestamp
with an explicit `sweepToCustodyTimestamp == 0` revert inside
sweepToCustody. Same protection against the unset-delay footgun,
without coupling finalize ordering to setCustodyDelay.

Test updated: footgun guard now asserts sweepToCustody reverts
with CustodyDelayNotElapsed when sweepToCustodyTimestamp is 0.
Comment thread contracts/MaticX.sol Outdated
Comment thread contracts/MaticX.sol Outdated
uint256 rate = preFinalizeRate == 0 ? 1 : preFinalizeRate;
uint256 balanceInMaticX = (_balance * TERMINAL_RATE_PRECISION) /
rate;
return (balanceInMaticX, TERMINAL_RATE_PRECISION, rate);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong format.

(balanceInMaticx, totalmaticxsupply, totalmaticOnContract)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread contracts/MaticX.sol Outdated
if (recallInitiated) {
uint256 rate = preFinalizeRate == 0 ? 1 : preFinalizeRate;
uint256 balanceInPOL = (_balance * rate) / TERMINAL_RATE_PRECISION;
return (balanceInPOL, TERMINAL_RATE_PRECISION, rate);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong format.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread contracts/MaticX.sol Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please route this to either revert() when recallInitiated but not recallCompelte and to instantUnstake after terminalratelocked please.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need InstantUnstake after this fn?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blockgroot would 100% prefer this backwards compatible change. Route this to "instantUnstake" after instantUnstakeEnabled flag please. Revert in other stages before this.

@blockgroot blockgroot May 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whenNotPaused modifier on requestWithdraw will block the function execution. moreover instantUnstake does not take any amount param.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any downside of keeping them decoupled, we did same setup earlier in BNBx as well and it's a small change on FE to facilitate legacy and new instant claims.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check slack

Comment thread contracts/MaticX.sol Outdated
mapping(address => uint256) public assetRecallNonces;
bool public recallInitiated;
uint256 public preFinalizeRate;
bool public recallClaimsComplete;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recallComplete*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread contracts/MaticX.sol Outdated
address vs = stakeManager.getValidatorContract(validatorIds[i]);
uint256 nonce = assetRecallNonces[vs];
if (nonce != 0) {
// Claim first, then pop: if the validator reverts (e.g.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread contracts/MaticX.sol Outdated
function claimAssetRecallNonces() external onlyRole(DEFAULT_ADMIN_ROLE) {
require(paused(), "Pause first");
if (!recallInitiated) revert RecallNotInitiated();
if (terminalRateLocked) revert TerminalRateAlreadyLocked();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have to check for recallClaimsComplete right?

@blockgroot blockgroot May 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually it's going to revert and also guarded by DEFAULT_ADMIN_ROLE.
also, terminalRateLocked require recallClaimsComplete so this check is going to be redundant.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added here 3e06115

Comment thread contracts/MaticX.sol
Comment thread contracts/MaticX.sol Outdated

uint256 polBal = polToken.balanceOf(address(this));
uint256 maticBal = maticToken.balanceOf(address(this));
recalledPolBalance = 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any custody movement (any token), we can stop instant unstake.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread contracts/MaticX.sol
Per review stader-labs#2: recalledPolBalance is mathematically redundant. After
finalize, terminalRate = balance / supply. Each instantClaim burns m
MATICx and transfers m * rate, so supply and balance decrement
proportionally — the implied rate is invariant. balanceOf is a single
source of truth, no drift, one fewer storage slot.

- Remove storage slot
- instantClaim: sufficiency check + payout read balanceOf live
- pushTerminalRateToL2: reads balanceOf live (idempotent under
  proportional drain)
- sweepToCustody: drop zero-out
- AssetRecallCompleted event already takes polBalance (no change);
  TerminalRatePushedToL2 2nd arg renamed to polBalanceAtPush
Per review stader-labs#3 + stader-labs#4: _convertMaticXToPOL and _convertPOLToMaticX
returned (balance, TERMINAL_RATE_PRECISION, rate) in the recall and
post-finalize branches, violating the natspec contract of
(balance, totalShares, totalPooled).

Return derived (totalShares, totalPooled) where totalPooled =
totalShares * rate / 1e18. The implied rate (totalPooled / totalShares)
equals the locked rate exactly — donation-immune, semantics intact.
Per review stader-labs#6. Storage slot unchanged (same bool), only the public
getter selector changes.
Per review stader-labs#7. Call sellVoucher_newPOL first, then read the
post-incremented unbondNonces canonically — same pattern as the
legacy requestWithdraw path at L308-315. Drops the +1 prediction.
…bs#9)

Once the claim phase succeeds (recallComplete = true), block re-entry
with RecallAlreadyComplete. Tightens the one-way state machine:
initiated -> complete -> finalize. Prior idempotent no-op behavior
on post-completion calls is now an explicit revert.
…-labs#10/stader-labs#11/stader-labs#12)

sweepToCustody now takes (asset, custody) and moves the full balance of
any ERC20. First call (any asset) flips `assetCustodied`, which
permanently disables `instantClaim` so the post-custody residue can't
be re-entered.
Comment thread contracts/MaticX.sol Outdated
require(paused(), "Pause first");
if (!recallInitiated) revert RecallNotInitiated();
if (recallComplete) revert RecallAlreadyComplete();
if (terminalRateLocked) revert TerminalRateAlreadyLocked();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need terminalRateLocked anymore. Dead stmt.

Comment thread contracts/MaticX.sol Outdated
/// Dust remaining in validators is forfeit (not user funds — terminal rate
/// is computed from POL balance only).
function finalizeTerminalRate() external onlyRole(DEFAULT_ADMIN_ROLE) {
require(paused(), "Pause first");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't thinking through this migration in a right way. Lots of redundant steps and multidirection flows (primary reason for bugs).

  1. Pause protocol
  2. initiate recall - you need to just check for pausing (which is prerequisite) and recallInitiated (so you can't call this again).
  3. Complete Recall - you need to just check for recallInitiated (so you can call completeRecall) and for completeRecall (so you can't call this again). How can you guarantee that we are not prematurely calling this fn?
  4. FreezeRate - again check just for previous step and the current step
  5. PushTOL2
  6. setInstantRedeemEnabled
  7. All good?
  8. Asset Custody

Comment thread contracts/MaticX.sol Outdated
// During recall (post-bulkUnstake, pre-finalize): serve the
// pre-recall snapshot so oracle does not drift toward zero as
// validators unbond.
if (recallInitiated) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we truly need this?

Users cannot deposit, withdraw after pausing or even recallInitiated, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with preFinalizeRate

Comment thread contracts/MaticX.sol
Comment thread contracts/MaticX.sol Outdated
// oracles cannot be moved by donations or recalled-balance burns.
// Return derived (shares, pooled) so totalPooled/totalShares ratio
// equals the locked rate exactly — donation-immune.
if (terminalRateLocked) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Abstract wih pre-finalize rate

Comment thread contracts/MaticX.sol Outdated
// Set only after the whole loop completes: if any per-validator
// claim reverts (unbond not yet matured), the entire tx reverts
// and this flag stays false so the txn can be retried.
recallComplete = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to top

Comment thread contracts/MaticX.sol Outdated
function setInstantRedeemEnabled(
bool _enabled
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (_enabled && !terminalRateLocked) revert TerminalRateNotLocked();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove enabled from check

Comment thread contracts/MaticX.sol Outdated
/// path post-sunset and it always exits the caller in full. Reverts with
/// `ZeroAmount` if the caller holds no MATICx.
/// Intentionally not gated by `whenNotPaused`.
function instantClaim() external nonReentrant {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take amount param

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.

2 participants