Skip to content

fix(staking): keep reserve-capacity floor on deposits - #5653

Closed
sbackend123 wants to merge 1 commit into
masterfrom
fix/staking-height-deposit-floor
Closed

sbackend123 wants to merge 1 commit into
masterfrom
fix/staking-height-deposit-floor

Conversation

@sbackend123

Copy link
Copy Markdown
Contributor

Checklist

  • I have read the coding guide.
  • My change requires a documentation update, and I have done it.
  • I have added tests to cover my changes.
  • I have added or updated fuzz targets for any code handling untrusted input.
  • I have filled out the description and linked the related issues.

Description

Deposits at height above zero must leave potential stake at least MIN_STAKE * 2^height. #5571 dropped that Bee-side floor when it started mirroring the contract. This puts the floor back inside calculateMinDeposit, next to the commitment check from #5571. If commitment requires more than the floor, the larger amount is still reported.

How the staking contract treats height

StakeRegistry.manageStake has two amount reverts, then writes height unconditionally.

The absolute floor applies only to the first stake. The comment in the contract says so, and the condition is _stakingSet == 0 (lastUpdatedBlockNumber == 0):

// First time adding stake, check the minimum is added, take into account height
if (_addAmount < MIN_STAKE * 2 ** _height && _stakingSet == 0) {
    revert BelowMinimumStake();
}

MIN_STAKE is 10^17. A new node at height 1 must deposit at least 2 * 10^17. An existing stake skips this check entirely.

After that, a non-zero deposit may not decrease commitment. Equality is allowed:

if (_addAmount > 0) {
    updatedPotentialStake = potentialStake + _addAmount;
    newCommittedStake = updatedPotentialStake / (currentPrice() * 2 ** _height);
    if (newCommittedStake < previousCommittedStake) {
        revert DecreasedCommitment();
    }
}

Height is then stored with no further comparison to MIN_STAKE * 2^height:

stakes[msg.sender] = Stake({
    potentialStake: updatedPotentialStake,
    committedStake: updatedCommittedStake,
    height: _height
});

A price drop can make that second check free. Stake 10^17 at price 48000 and height 0:

committed = 10^17 / 48000 = 2083333333333

Price then falls to 24000 and the node calls manageStake with height 1 and 1 PLUR:

denominator   = 24000 * 2^1 = 48000
newCommitted  = (10^17 + 1) / 48000 = 2083333333333

DecreasedCommitment does not fire, BelowMinimumStake does not apply, and the contract records height 1 with potential 10^17 + 1. A new node at that same height would have been rejected below 2 * 10^17. Redistribution then uses the stored height (depth - height), so the doubled reserve is credited without the stake a new node must post.

A zero-amount manageStake does not even recompute commitment. node.go already refuses that path (UpdateHeight) when reserve-capacity-doubling > 0 and potential is below 2^h * MIN. Every deposit also sends the configured height, and #5571 no longer blocked that write.

What #5571 removed, and what this restores

Before #5571, DepositStake rejected a deposit unless potential + deposit >= (1 << height) * MIN. That included height 0, so an existing stake that had fallen below MIN could not be topped up until it was back at MIN.

#5571 replaced that check with calculateMinDeposit, which copies the contract:

  • first stake: MIN * 2^height
  • later deposits: the gap that keeps committed * price * 2^height from decreasing, or 1 PLUR when there is no gap

That commitment math stays. It is why a price increase still raises minimumDeposit, and why an existing height-0 stake below MIN (slashed, or withdrawn to zero potential) can still deposit 1 PLUR. The contract accepts those, and GET /stake must keep reporting them.

What #5571 also dropped is the reserve-capacity floor for height above zero. This PR adds it back in the same function, after the commitment gap:

floor = MIN * 2^height
gap   = floor - potential
minimumDeposit = max(contract minimum, gap)    // only when height > 0

GET /stake and DepositStake therefore agree. In the price-drop case above, commitment allows 1 PLUR and the floor raises the reported minimum to 10^17, which brings potential to 2 * 10^17 before height 1 is published. If commitment already requires more than that gap, the commitment amount is kept.

Height 0 is unchanged on purpose. (1 << 0) * MIN is the old blanket floor, and restoring it would undo the #5571 rule that an existing stake below MIN is not forced back up. node.go applies the same limit only when reserve-capacity-doubling > 0.

Open API Spec Version Changes (if applicable)

None. GET /stake and POST /stake/{amount} keep the minimumDeposit field added in #5571. For height above zero the value can be higher than the contract's own minimum.

Motivation and Context (Optional)

#5571 correctly pre-validates BelowMinimumStake and DecreasedCommitment. The contract does not re-check MIN * 2^height once a stake exists, so mirroring it let a deposit publish a reserve-capacity height that node.go refuses to publish via UpdateHeight.

Related Issue (Optional)

Follow-up to #5571.

Screenshots (if appropriate):

AI Disclosure

  • This PR contains code that has been generated by an LLM.
  • I have reviewed the AI generated code thoroughly.
  • I possess the technical expertise to responsibly review the code generated in this PR.

Made with Cursor

@sbackend123

Copy link
Copy Markdown
Contributor Author

Closed: the description was meant to stay in review notes, not to open a pull request.

@sbackend123 sbackend123 closed this Oct 8, 2026
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