Skip to content

fix(staking): report minimum deposit when commitment would decrease - #5571

Open
sbackend123 wants to merge 7 commits into
masterfrom
fix/stake-min-deposit
Open

fix(staking): report minimum deposit when commitment would decrease#5571
sbackend123 wants to merge 7 commits into
masterfrom
fix/stake-min-deposit

Conversation

@sbackend123

@sbackend123 sbackend123 commented Aug 23, 2026

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 filled out the description and linked the related issues.

Description

  • Fixes #5193: small stake deposits that would decrease commitment no longer fail with a misleading intrinsic gas too low / HTTP 500.
  • Pre-validates deposits against the staking contract rules (initial MIN_STAKE * 2^height floor and non-decreasing commitment using the price oracle) and returns HTTP 400 with the required minimumDeposit.
  • Exposes minimumDeposit on GET /stake.
  • Resolves the price oracle address from the staking contract (OracleContract) at node startup, so custom/overridden staking contracts use the correct oracle.

Open API Spec Version Changes (if applicable)

  • GET /stake → { "stakedAmount": "...", "minimumDeposit": "..." }
  • POST /stake/{amount} on insufficient deposit → 400 with { "code": 400, "message": "insufficient stake amount", "minimumDeposit": "..." }
  • OpenAPI bumped to 8.2.0

Motivation and Context (Optional)

Related Issue (Optional)

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.

Pre-check manageStake against the non-decreasing commitment rule so
small deposits fail with HTTP 400 and the required amount instead of a
misleading gas error. Expose the minimum on GET /stake.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sbackend123
sbackend123 marked this pull request as ready for review August 24, 2026 13:18
func calculateMinDeposit(potential, committed *big.Int, price uint32, height uint8) *big.Int {
minAdd := big.NewInt(1)

// The contract applies the minimum stake floor only when creating a stake.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The staking contract applies the initial minimum based on:

_addAmount < MIN_STAKE * 2 ** _height && _stakingSet == 0

That is, it checks lastUpdatedBlockNumber == 0, not whether potentialStake == 0.

A possible state is:

  1. A stake was previously created, so lastUpdatedBlockNumber != 0.
  2. The effective/committed stake later becomes zero.
  3. The potential stake is withdrawn, leaving potentialStake == 0.
  4. A subsequent deposit is made.

In that state, the contract does not apply the initial MIN_STAKE * 2^height floor, but calculateMinDeposit reports that floor anyway. The API can therefore overstate the required minimum and reject deposits that the contract would accept.

The calculation needs access to the stake’s lastUpdatedBlockNumber or an equivalent “stake has previously been initialized” value. The current getStake helper only returns committed and potential stake.

Suggested fix:

  • Return lastUpdatedBlockNumber from getStake.
  • Pass an isInitialized/stakingSet boolean into calculateMinDeposit.
  • Mirror the contract’s _stakingSet == 0 condition exactly.

A test should cover potentialStake == 0 with lastUpdatedBlockNumber != 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.

Looks like mix of different scenarios to me, wrote in masttermost for further discussion

Comment thread pkg/node/node.go Outdated
}

stakingContract := staking.New(overlayEthAddress, stakingContractAddress, abiutil.MustParseABI(chainCfg.StakingABI), bzzTokenAddress, transactionService, common.BytesToHash(nonce), contractGasLimit, uint8(o.ReserveCapacityDoubling))
stakingContractABI := abiutil.MustParseABI(chainCfg.StakingABI)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

L1227-L1235
I see a bunch of issues here. This staking contract initialization is leaking unnecessary details. Now pkg/node is responsible for calling a staking-package helper to discover an internal dependency and passing it back to the staking constructor.

If staking contract changes its oracle discovery logic tomorrow, we will have to change pkg/node as well. If chainEnabled is false, we are passing zero address to staking constructor which may end up using this in GetMinDeposit func call.

I see two options:

  1. Either the constructor should call this function and return error OR
  2. We do lazy oracle resolution. i.e we resolve the oracle contract in GetMinDeposit when it needs the current price.

I would prefer the second option.

Second issue is along with chainEnabled we have storage incentives enabled check as well. We could have nodes that enable chain but not storage incentives. So it is better to do this lazily inside staking contract when GetMinDeposit is called.

// User already has committed stake.
// Commitment protection: required = committed * price * 2^height
required := new(big.Int).SetUint64(uint64(price))
required.Lsh(required, uint(height)) // * 2^height

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: although this will work as is, I would usually prefer assigning the result into a new variable or the same variable. According to me it is clearer and reduces maintenance risk. This is the style we usually followed in the codebase in general.

For eg. you can look at pkg/accounting/accounting.go:245, :251, and :257

@sbackend123
sbackend123 marked this pull request as draft August 26, 2026 12:15
@sbackend123
sbackend123 removed the request for review from martinconic August 26, 2026 12:15
@sbackend123
sbackend123 marked this pull request as ready for review August 26, 2026 16:16
# Conflicts:
#	openapi/Swarm.yaml

// getPriceOracleAddress resolves the price oracle from the staking contract on first use.
func (c *contract) getPriceOracleAddress(ctx context.Context) (common.Address, error) {
if (c.priceOracleAddress != common.Address{}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be protected against concurrent access.

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.

Unclear error message when depositing stake and commitment decreases

2 participants