
On this page▾
A staking contract can hold enough tokens for the next withdrawal and still be insolvent for everyone who remains.
The reason is not necessarily a broken transfer or reward formula. It can be a category error. When principal deposits and reward funding use the same token,
balanceOf(address(this)) reports custody, not the obligations attached to that custody.TL;DR
- Source fact: Zealynx's Ample finding reports that one staking balance did not distinguish user principal from rewards. Earlier reward payments could therefore consume tokens needed for later principal withdrawals.[1]
- Comparative evidence: A Code4rena Locke finding shows a related same-token failure in recovery logic, while Velodrome and Ajna findings show adjacent ways reward liabilities can exceed or disappear against available funding.[3][4][5]
- Zealynx classification: These records form three distinct classes: reserve aliasing, over-accrual, and destructive shortfall handling.
- Practical rule: A staking system should track assets by obligation and test that every valid claim sequence preserves principal plus accrued reward liabilities.
The Ample case study contains the engagement narrative. This article removes the client-specific frame and tests the underlying reserve mechanism against other public technical records.
Methodology and evidence boundary
This is a mechanism-led comparison, not a prevalence study. A record entered the comparison if its public source described at least one of these conditions:
- one token balance served multiple obligations without a correct aggregate reserve calculation;
- reward accounting created liabilities beyond the funded reward amount; or
- a shortfall path erased a user's remaining claim instead of preserving it.
The core record includes the Ample finding, Locke Streaming issue #215, Velodrome issue #186, and Ajna issue #135. The Synthetix-derived staking model is included as a defensive comparison because its public implementation pattern explicitly bounds a reward rate against token balance.[6]
The statements below use four labels:
- Source fact: what a cited source reports.
- Zealynx classification: how this article groups the mechanism.
- Inference: a conclusion derived across the selected records.
- Limitation: what the evidence does not establish.
Contest labels are not normalized into a new severity scale. Duplicate status, judge commentary, and source-reported remediation remain part of their original context.
The invariant: assets must cover obligations, not transfers
For a staking contract using one token for both stake and reward, the useful inequality is:
1actual token assets2 >=3principal owed to open positions4 + accrued reward liabilities5 + other reserved claims
A stricter promise-time model also accounts for rewards that have been committed by an active schedule but have not accrued yet:
1free reward reserve2 =3actual token assets4 - principal liability5 - accrued reward liability6 - other reserved claims
A new reward promise is safe only if its maximum additional liability fits inside that free reserve.
This is a protocol solvency statement. It is different from asking whether the next ERC-20 transfer can execute. Transfer success proves that the sender had enough tokens for one payment. It does not prove that the remaining assets cover everyone else.
Why one ERC-20 balance loses economic meaning
Consider a contract with two stakers and one reward funder:
1principal deposited: 200 tokens2reward funding: 10 tokens3contract balance: 210 tokens4promised rewards: 40 tokens5required assets: 240 tokens6shortfall: 30 tokens
The contract can pay one user 120 tokens. That transfer succeeds. The remaining balance is 90 tokens, while the second user's principal alone is 100 tokens.
These numbers are an illustrative model, not an observed transaction or Ample balance. The point is structural: an ERC-20 balance does not contain labels. The contract must maintain the labels in its own state or derive them from complete liabilities.
The mistake often appears in one of four places:
- funding logic treats every incoming token as new reward capacity;
- reward scheduling checks total balance without subtracting principal;
- payout logic sends principal plus reward without reducing a reward reserve;
- recovery logic calculates the same physical balance as excess under two accounting categories.
Anchor record: Ample's reserve aliasing
Zealynx finding F-2025-0001 reports that Ample's staking contract did not differentiate staked tokens from reward tokens in its balance. According to the public finding, earlier rewards could consume staked principal and leave later withdrawals underfunded.[1]
The public audit record identifies the reviewed scope as one 125-line Solidity contract at commit
45f12c5d992d82c14166aba34a455fe9412045ba. It reports the finding as High severity and fixed, with verification by Zealynx.[2]Zealynx classifies the mechanism as reserve aliasing:
1one physical token balance2+ two economic obligations3- aggregate liability accounting4=5ambiguous spendable reserve
The report's recommended direction was separate reward accounting, updates when positions close, and observable reward metrics.[1] "Fixed and verified" is a report status for the reviewed remediation. It does not independently establish deployment, later-code security, or that exploitation occurred.
Exact analogue: same-token recovery counted excess twice
Code4rena issue #215 for Locke's Streaming contract is not a staking reward schedule, but it is a close accounting analogue. The source reports that deposit and reward amounts were tracked independently while both could refer to the same ERC-20 token. Recovery logic then evaluated that physical balance once as the deposit token and again as the reward token.[3]
The issue's worked example assigns 1,000 units to remaining deposits and 500 units to rewards. Because the recovery branches considered the same balance independently, the creator could recover value twice and leave user deposits or rewards unavailable. The proposed mitigation calculated excess only after subtracting both obligations from the shared balance.[3]
Zealynx classifies this as double interpretation of one reserve. Ample lacked sufficient separation during reward payment. Locke had separate counters but failed to combine them when deciding what was recoverable. The code shapes differ. The invariant is the same:
1recoverable excess2 =3actual assets4 - all obligations denominated in that asset
A token address can occupy several business roles. It remains one pool of physical assets.
Adjacent mechanism: reward schedules can over-accrue
Velodrome issue #186 reports a different path to the same terminal condition. Its source describes a mismatch between reward duration and period finish when updating
rewardRate. The reported result was that aggregate user rewards could exceed available reward tokens, leaving later claimers unable to receive rewards.[4]The judge commentary treated the issue as Medium severity and noted that a later reward round might make some users whole, while permanent loss remained possible if no later funding arrived.[4]
Zealynx classifies this as liability creation beyond funded reserve, not reserve aliasing. Principal and rewards do not need to share a token for this class to exist. The schedule itself can create too much reward debt.
The distinction matters during review:
- reserve aliasing asks whether assets assigned to principal are being mistaken for rewards;
- over-accrual asks whether the reward ledger promises more than reward funding can cover.
Both must be tested. Fixing one does not fix the other.
Adjacent mechanism: a shortfall must not erase debt
Ajna issue #135 reports that when
RewardsManager lacked enough AJNA to pay a calculated reward, the claim was truncated to the current contract balance. The issue was marked as a duplicate, judged High severity, and marked satisfactory in the source thread.[5]If a user was owed more than the available balance, the unpaid portion could be lost rather than retained as a claim. The source recommendation was to support partial claims while preserving the remainder.[5]
Zealynx classifies this as destructive shortfall handling. The shortfall may arise from funding, accounting, or another defect. The claim path then converts a temporary liquidity problem into permanent accounting loss by clearing more liability than it pays.
A safe partial-payment relationship is:
1paid = min(owed, available reward assets)2remaining debt = owed - paid
The state transition must reduce the user's claim by
paid, not by owed.Comparative matrix
| Record | Source-reported mechanism | Zealynx classification | What must be reserved | Evidence limit |
|---|---|---|---|---|
| Ample F-2025-0001 [1][2] | Principal and rewards share an undifferentiated balance | Reserve aliasing | All open principal plus reward liabilities | No public evidence of exploitation or deployment |
| Locke Streaming #215 [3] | Same token balance is treated separately in deposit and reward recovery branches | Double interpretation of one reserve | Deposits plus rewards plus fees | Streaming system, not the same staking lifecycle |
| Velodrome #186 [4] | Reward-rate timing can accrue more reward than funded | Liability creation beyond reserve | Maximum aggregate reward debt | Judge described some loss as potentially temporary |
| Ajna #135 [5] | Insufficient-balance claim truncates and can erase the remainder | Destructive shortfall handling | Unpaid reward debt after partial payment | Duplicate issue, not an independent deployment study |
| Synthetix-derived pattern [6] | Reward rate is bounded against available reward balance | Defensive funding bound | Scheduled reward amount | A balance-only bound needs adjustment if reward and stake tokens are identical |
The comparison does not establish equal severity, exploitability, or remediation status. It shows that staking solvency can fail at reserve classification, schedule creation, recovery, or claim settlement.
The subtle limit of a balance-based funding check
A common Synthetix-derived pattern checks:
1require(2 rewardRate * duration <= rewardsToken.balanceOf(address(this)),3 "reward amount > balance"4);
The pattern is useful when the reward balance is entirely available for rewards.[6] It prevents a newly configured schedule from distributing more than the contract currently holds.
But if
rewardsToken == stakingToken, the right side may include user principal. The check can pass while the reward schedule is economically underfunded.The same-token version needs a net reserve:
1uint256 freeRewards = token.balanceOf(address(this))2 - principalLiability3 - accruedRewardLiability4 - otherReservedClaims;56require(maxAdditionalRewardLiability <= freeRewards);
This is an inference from the comparison, not a claim that the cited Synthetix-derived implementation supports same-token staking. The design rule is broader: every funding bound must subtract obligations that cannot fund the new promise.
Design patterns that preserve principal
Track liabilities explicitly
At minimum, maintain enough state to derive:
1total principal owed2total accrued rewards owed3maximum additional rewards committed by active schedules4reserved administrative or fee claims5free reward reserve
Per-user state alone may be insufficient for on-chain checks if calculating the aggregate requires iterating every position. Aggregate counters should move with every lifecycle transition.
Reserve rewards when the promise is created
A contract can reserve rewards at one of two points:
- Schedule time: reserve the maximum distributable amount when a reward period starts.
- Position time: reserve the maximum reward when a fixed-rate position opens.
Either model can work. Mixing them without a conservation equation makes capacity difficult to prove.
Treat token recovery as a solvency operation
Recovery is not administrative housekeeping. For each token, calculate all obligations denominated in that token before declaring any excess.
A safe relationship is:
1recoverable <= actual balance - total reserved obligations
If the result is negative, recovery must be zero and the system is already underfunded.
Preserve unpaid liabilities
If partial claims are allowed, unpaid debt must survive. If partial claims are not allowed, revert without changing claim state. Never zero a complete entitlement after transferring only the available fraction.
Expose reserve state
Useful views include:
- principal liability;
- accrued reward liability;
- committed but unaccrued rewards;
- actual token assets;
- free reward reserve;
- current shortfall, if any.
Observability does not enforce solvency, but it lets monitors and users check the same invariant the contract is expected to preserve.
Tests that catch the first-claimer advantage
Ordinary unit tests often fund a contract generously and close one position. That proves the happy path, not solvency.
Use these tests instead:
- Same-token configuration: Set staking and reward token to the same address.
- Minimum reward funding: Fund only the exact permitted reserve.
- Multiple positions: Open positions with different sizes, durations, and boosts.
- Every exit order: Close the largest reward first, smallest first, and in randomized order.
- Aggregate assertion: After every transition, assert assets cover principal and accrued rewards.
- Maximum promise assertion: Include rewards committed by active schedules if the contract guarantees them.
- Partial claim: Underfund the reward side deliberately and verify unpaid debt remains.
- Recovery test: Attempt to recover the shared token while principal and rewards remain owed.
- Donation test: Transfer tokens directly to the contract and verify the accounting policy is explicit.
- Fee-on-transfer test: If supported, account from actual balance deltas rather than requested transfer amounts.
- Administrative withdrawal: Verify privileged paths cannot remove principal or committed rewards.
- Fuzzed sequence: Randomize stake, fund, claim, close, recover, and direct-transfer operations.
A compact invariant test is:
1function invariant_assetsCoverRecognizedLiabilities() public view {2 uint256 assets = token.balanceOf(address(staking));3 uint256 liabilities = staking.totalPrincipal()4 + staking.totalAccruedRewards()5 + staking.otherReservedClaims();67 assertGe(assets, liabilities);8}
If future rewards are guaranteed once scheduled, add the unaccrued committed amount. If they are explicitly conditional, document that weaker promise rather than silently assuming full funding.
This complements the decomposition method in Divide and Conquer Auditing and the transaction-level conservation model in When Every Check Passes but a Protocol Is Underbacked. The first isolates liabilities before reassembling the system. The second shows why locally valid operations can still violate an aggregate reserve boundary.
What this research does not establish
This corpus is purposive and small. It cannot support a claim about how common staking reserve failures are.
The article does not independently reproduce every cited proof of concept. It relies on the cited public technical records for their reported mechanisms and labels.
A recorded finding does not establish that vulnerable code was deployed, exploited, or associated with a monetary loss. No such claim is made for Ample.
The records are not identical. Locke concerns same-token stream recovery. Velodrome concerns reward-rate timing. Ajna concerns shortfall settlement. They are included to test different stages of one liability lifecycle, not to imply one shared implementation bug.
Finally, the invariant depends on the protocol's actual promise. Some systems distribute only what is funded and make no guarantee about future emissions. Others promise fixed returns at position creation. The liability model must reflect the documented promise, then the code and monitoring must enforce it.
The durable rule: separate custody from availability
The selected records point to one operational rule:
A token held by a contract is available for a new purpose only after every prior obligation denominated in that token has been subtracted.
Ample shows what happens when principal and rewards are not separated.[1] Locke shows that separate counters still fail if recovery does not aggregate them.[3] Velodrome shows that reward liabilities can outrun funding through schedule arithmetic.[4] Ajna shows that a shortfall path can erase the unpaid remainder.[5]
Staking solvency is therefore not one check. It is a lifecycle property from funding to promise creation, accrual, claim, position closure, and recovery. Systems that add delegation, slashing, or liquid wrappers need a wider threat model, covered separately in Restaking Risk.
Frequently asked questions
Can staking and reward tokens safely be the same token?
Yes, but the contract must account for principal and reward obligations separately and aggregate them whenever it schedules rewards, pays claims, or recovers excess tokens. A raw token balance is not sufficient.
Does a successful withdrawal prove the staking contract is solvent?
No. It proves only that one transfer could execute. Solvency requires the remaining assets to cover all remaining principal and reward liabilities regardless of valid withdrawal order.
Should future rewards count as liabilities?
Count rewards according to the protocol's promise. If a schedule or position guarantees them, reserve the maximum committed amount. If rewards are conditional on later funding, make that condition explicit in code and documentation.
What should happen when reward funding is temporarily insufficient?
Either revert without changing the user's claim or pay partially and preserve the unpaid remainder. Do not clear a complete entitlement after transferring only part of it.
What is the most useful monitoring metric?
Track actual assets minus principal, accrued rewards, committed rewards, and other reserved claims. Alert before the resulting free reserve becomes negative, not only after a transfer fails.
Glossary
| Term | Meaning in this study |
|---|---|
| Protocol solvency | The property that assets cover the obligations the protocol has recognized. |
| Invariant | A property that must remain true across every valid state transition. |
| Invariant testing | Stateful testing that searches operation sequences for a property violation. |
| Balance delta | The measured change in token custody across an operation, useful when requested and received amounts may differ. |
| Solidity | The contract language used by the selected staking and reward-accounting implementations. |
Sources
[1] https://zealynx.io/audits/ample-protocol/ample-staking-q2-2025/findings/F-2025-0001 - Zealynx, Ample finding F-2025-0001
[2] https://zealynx.io/audits/ample-protocol/ample-staking-q2-2025 - Zealynx, Ample public audit record
[3] https://github.com/code-423n4/2021-11-streaming-findings/issues/215 - Code4rena, Locke Streaming issue #215
[4] https://github.com/code-423n4/2022-05-velodrome-findings/issues/186 - Code4rena, Velodrome issue #186
[5] https://github.com/code-423n4/2023-05-ajna-findings/issues/135 - Code4rena, Ajna issue #135
[6] https://www.cyfrin.io/glossary/staking-rewards-solidity-code-example - Cyfrin, Synthetix-derived staking rewards implementation
Working auditors in your corner, all year
Zealynx Insiders: weekly live sessions, 1:1 advisory, pair-auditing, and Krait runs on your code, from an audit firm that publishes its reports.
No spam. Unsubscribe anytime.
Building a protocol? Start with a three-day pre-audit, credited toward your audit →
Keep reading
