Skip to content
Quantix Security Labs

Risk summary

State at remediation commit 8cd82e7b6c82

Findings
6
Critical and high
2
Open or partial
2
Remediation
75%

Findings by severity

  • Critical0
  • High2
  • Medium2
  • Low1
  • Informational1

Remediation statusAnalysis

  • Resolved3
  • Partially resolved1
  • Acknowledged1
  • Open1

Tamarind Staking

Staking

Document
QX-TSA-2026-006 · v1.0
Type
Smart Contract Audit
Review
20 Apr 2026 – 29 Apr 2026
Published
12 May 2026
Ecosystems
AptosSui
Languages
Move (Aptos), Move (Sui)
Risk Index
BB74.8
Assessment contents · 12 sections

01

Assessment overview

Tamarind lets users stake the native asset for a receipt token, accrue rewards through a reward-per-share index, and withdraw through a cooldown queue. In this design the critical invariant is that every user’s accrued rewards are settled at the index value in force when their stake changed.

Six findings were reported. Both high-severity issues were accounting bugs: stake changes did not settle the reward index first, so new stake earned rewards accrued before it arrived, and the cooldown could be bypassed by splitting a position. Both were fixed.

Unbounded iteration in batch claims was only partially addressed, rounding dust was acknowledged, and an admin capability that can change the cooldown with immediate effect remains open. Those open items are reflected in the protocol’s governance and code-security components.

02

Technical analysis: protocol architecture

Users call stake to deposit the native coin into the pool and receive a receipt token at the current exchange rate. Validator rewards arrive through distribute, which increases a global reward_per_share index.

Each position stores reward_debt, the index value at which it was last settled. Pending rewards are shares × (index − reward_debt). Claims pay pending rewards and reset the debt.

request_unstake moves shares into the unstake queue with an unlock timestamp; withdraw pays out once the cooldown has passed. Parameters, including cooldown length and reward rate, are changed by the holder of AdminCap.

Trusted· 4

Assumed to behave as specified; compromise is out of the threat model.

  • staking_poolStake, unstake and receipt-token accounting
  • reward_indexReward-per-share accrual and claims
  • unstake_queueCooldown and withdrawal processing
  • admin_capCapability object for parameter updates
trust boundary

Semi-trusted· 1

Trusted for liveness or within bounds; misbehaviour is partly modelled.

  • Reward distributorCalls distribute with validator rewards
trust boundary

Untrusted· 1

May act adversarially; every input is validated.

  • StakersStake, split, merge, claim and unstake
Components grouped by trust level. Calls that cross a trust boundary are where the review concentrated input validation and authorisation checks.

Trust assumptions

  • The AdminCap is held by a multisig account controlled by the protocol team.
  • The reward distributor reports rewards honestly; incorrect reports affect the rate, not individual balances.
  • Aptos framework coin and timestamp modules behave as documented.

03

Scope

Aptos Move modules for staking, reward-index accounting, the unstake cooldown queue and the admin capability.

Repository
Private repository
Review effort
Two reviewers, eight working days
Reviewed commit
00af372ccd088a27187d390b402ef9d23d19824b
Remediation commit
8cd82e7b6c829eee4d5ea44bc796ddcc9a8aa238
Files and components in scope
PathDescriptionnSLOC
sources/staking_pool.moveStake, unstake and receipt-token accounting412
sources/rewards.moveReward-per-share index, accrual and claims286
sources/unstake_queue.moveCooldown queue and withdrawal processing238
sources/admin.moveAdminCap capability and parameter updates96
Total4 items1,032

Out of scope

  • The Sui port (compared for parity of the TS-01 and TS-02 fixes only)
  • Validator selection and delegation off-chain
  • Aptos framework modules (assumed to behave as documented)

04

Threat model

Each threat lists the actor, the vector and the mitigation, and links to the findings that realised it in the reviewed code.

Assets at risk

  • Staked native coin
  • Undistributed and accrued rewards
  • Cooldown guarantees relied on by integrators

Actors

  • StakerUntrustedStakes, splits positions, claims and unstakes
  • AdminTrustedHolds AdminCap; updates parameters
  • DistributorSemi-trustedCalls distribute
Threats, vectors, mitigations and related findings
ThreatActorVectorMitigationFindings
Rewards captured by late stakersStakerStaking just before distribute is settledSettle index before stake changesTS-01
Cooldown bypassStakerSplitting a queued positionCarry unlock time with sharesTS-02
Claim function unusableStakerMany small positions in a batchBounded batch sizeTS-03
Cooldown changed under usersAdminImmediate parameter updateDelayed parameter changesTS-06

05

Findings

Severity

6 findings

TS-01HighResolvedBusiness logic

Reward index is not settled before stake changes

sources/staking_pool.move:L118–L156

Likelihood
High
Impact
Medium
Weakness
CWE-682
Description

stake increases a position’s shares without first paying out pending rewards or moving reward_debt to the current index. Pending rewards are later computed as shares × (index − reward_debt), so the newly added shares earn every increment of the index since the position was last settled.

The same defect exists in the reverse direction in request_unstake, where shares removed before settlement forfeit their accrued rewards.

Preconditions
  • An existing position with an old reward_debt
  • Rewards distributed since that position was last settled
Exploit scenario
  1. 1.Attacker opens a tiny position early, so its reward_debt is very low.
  2. 2.Months later, just after a large distribution, they stake a large amount into the same position.
  3. 3.They claim; the large stake is paid as if it had been staked since the beginning.
Impact

Rewards owed to long-term stakers are captured by late stakers; the pool becomes unable to pay all claims.

Recommendation

Call an internal settle(position) at the start of every function that changes shares: pay or credit pending rewards, then set reward_debt = index.

Fix
sources/staking_pool.movediff
   public entry fun stake(user: &signer, amount: u64) acquires Pool, Position {       let pos = borrow_global_mut<Position>(signer::address_of(user));+      rewards::settle(pos);       let shares = shares_for(amount);       pos.shares = pos.shares + shares;
Resolution

settle() is called first in stake, request_unstake, split and merge. MP-02 proved.

TS-02HighResolvedBusiness logic

Cooldown can be bypassed by splitting a queued position

sources/unstake_queue.move:L64–L102

Likelihood
High
Impact
Medium
Weakness
CWE-841
Description

split creates a new position from part of an existing one. When the source position has queued shares, the new position receives them with unlock_at set to zero, because the field is initialised by default rather than copied.

A user can request unstake, immediately split the queued position, and withdraw from the new half without waiting for the cooldown.

Preconditions
  • A position with queued shares
Exploit scenario
  1. 1.User requests unstake of all shares.
  2. 2.User splits the position in the same block.
  3. 3.User withdraws from the new position immediately.
Impact

The cooldown no longer protects the pool’s liquidity planning or integrators that rely on it.

Recommendation

Copy unlock_at with queued shares in split, and in merge take the later of the two unlock times.

Fix
sources/unstake_queue.movediff
   let new_entry = QueueEntry {       shares: amount,-      unlock_at: 0,+      unlock_at: entry.unlock_at,   };
Resolution

unlock_at copied on split and maximised on merge. MP-03 proved; the Sui port carries the same fix.

TS-03MediumPartially resolvedDenial of service

Batch claim iterates over an unbounded list

sources/rewards.move:L201

Likelihood
Medium
Impact
Medium
Weakness
CWE-400
Description

batch_claim iterates over every queue entry owned by the caller. Users who unstake in many small steps, or integrators who manage many positions under one account, accumulate entries until the function exceeds the transaction gas limit.

Because entries are only removed by a successful claim, an account that crosses the limit cannot reduce its list through this function.

Preconditions
  • An account with many queue entries
Exploit scenario
  1. 1.An integrator account accumulates thousands of entries.
  2. 2.batch_claim always runs out of gas; rewards are stuck until a manual path exists.
Impact

Rewards and withdrawals for affected accounts become unclaimable through the batch path.

Recommendation

Add a max_entries argument and process the list in pages, or cap the number of entries per account.

Resolution

Partially resolved. A max_entries argument was added, but entries are still stored in a vector that is fully loaded on each call; a table-based store is planned.

TS-04LowAcknowledgedPrecision and rounding

Rounding leaves undistributed dust in the reward pool

sources/rewards.move:L88

Likelihood
Medium
Impact
Low
Weakness
CWE-1339
Description

distribute computes amount × PRECISION / total_shares with truncation. The remainder is never carried forward, so a small amount of each distribution can never be claimed.

Preconditions
  • Any distribution not divisible by total shares
Exploit scenario
  1. 1.Over many distributions, unclaimable dust accumulates in the pool.
Impact

Small, permanent loss of rewards to stakers; no exploitation path.

Recommendation

Carry the remainder forward to the next distribution.

Resolution

Acknowledged. The team will sweep dust to the treasury periodically and document it.

TS-05InformationalResolvedCode quality

Parameter updates emit no events

sources/admin.move:L40

Likelihood
Low
Impact
Low
Description

Changes to cooldown length, reward rate and fee are applied without events, so indexers and monitoring cannot observe them.

Preconditions
  • None
Exploit scenario
  1. 1.A parameter change goes unnoticed by users and integrators.
Impact

Reduced transparency of administrative actions.

Recommendation

Emit a module event for every parameter change, including old and new values.

Resolution

Events added for all parameter changes.

TS-06MediumOpenGovernance manipulation

AdminCap can change the cooldown with immediate effect

sources/admin.move:L58

Likelihood
Low
Impact
High
Description

set_cooldown(cap, seconds) updates the cooldown immediately and applies to entries already in the queue, because withdraw compares against requested_at + cooldown using the current value.

The holder of the capability, or anyone who compromises it, can lengthen the cooldown to trap queued withdrawals, or shorten it to zero.

Preconditions
  • Control of AdminCap
Exploit scenario
  1. 1.The capability holder raises the cooldown to a year.
  2. 2.All queued withdrawals are frozen without notice.
Impact

Users cannot rely on the cooldown they accepted when they requested unstake.

Recommendation

Record the unlock time at request time so later changes apply only to new requests, and queue parameter changes behind a delay.

Resolution

Open. The team intends to store unlock_at at request time in the next release.

06

Severity

6findings
  • Critical0 · 0%
  • High2 · 33%
  • Medium2 · 33%
  • Low1 · 17%
  • Informational1 · 17%

By vulnerability class

  • Business logic2
  • Code quality1
  • Denial of service1
  • Governance manipulation1
  • Precision and rounding1

Likelihood and impact

Each finding placed by the likelihood of its preconditions and the impact of exploitation.

Findings by likelihood (columns) and impact (rows)
Impact ↓ / Likelihood →LowMediumHigh
High
0
0
Medium
0
Low
0

07

Remediation

3 of 6 findings are resolved, 1 acknowledged, 1 partially resolved and 1 open at remediation commit 8cd82e7b6c82.

Remediation progress75%

Resolved and acknowledged findings count fully; partially resolved findings count half.

75%remediated
  • Resolved3 · 50%
  • Partially resolved1 · 17%
  • Acknowledged1 · 17%
  • Open1 · 17%
Findings by severity and remediation status
SeverityResolvedPartially resolvedAcknowledgedOpenTotal
Critical00000
High20002
Medium01012
Low00101
Informational10001
All findings31116

Residual items

  • TS-03MediumPartially resolvedPartially resolved. A max_entries argument was added, but entries are still stored in a vector that is fully loaded on each call; a table-based store is planned.
  • TS-04LowAcknowledgedAcknowledged. The team will sweep dust to the treasury periodically and document it.
  • TS-06MediumOpenOpen. The team intends to store unlock_at at request time in the next release.
  • ResolvedFixed and verified in the remediation review.
  • Partially resolvedMitigated; the residual risk is documented.
  • AcknowledgedAccepted by the protocol team with a stated rationale.
  • OpenNot yet addressed at the reviewed remediation commit.

08

Formal verification

Core accounting properties were specified for the Move Prover on the remediation commit. Properties involving the unstake queue’s iteration were not proved because the prover times out on the unbounded loop.

Tool: Move Prover

  • MP-01ProvedTotal receipt shares equal the sum of position shares plus queued shares
  • MP-02ProvedA position’s reward_debt equals the current index after any stake, unstake or claimViolated on the original commit (TS-01).
  • MP-03ProvedShares can leave the queue only after their own unlock timestampViolated on the original commit (TS-02).
  • MP-04Inconclusivebatch_claim terminates within the gas limit for any inputProver timeout; see TS-03.

A proved property holds for every execution of the model under its stated assumptions. Fuzzing and property-based results are evidence, not proofs, and are recorded as inconclusive.

09

Recommendations

  1. 01

    Fix TS-06 before integrations rely on the cooldown

    Store unlock times at request and delay parameter changes so integrators can quote withdrawal times reliably.

  2. 02

    Move the queue to a table

    Replace the vector-backed queue with a table keyed by entry ID so batch operations are bounded and MP-04 can be proved.

  3. 03

    Keep the two ports in step

    Maintain a parity checklist between the Aptos and Sui modules and run the same scenario tests against both.

10

Methodology and limitations

  1. 01Manual review of every public entry function and resource transfer
  2. 02Reward accounting review against the reward-per-share model, including stake changes between distributions
  3. 03Move Prover specifications for the core accounting invariants
  4. 04Unit and scenario tests of split, merge and unstake sequences
  5. 05Remediation review against the fix commit, with parity checks of the TS-01 and TS-02 fixes in the Sui port

Severity classification

Critical
Direct loss of funds or permanent loss of control is achievable with little or no precondition.
High
Material loss or protocol disruption is achievable under realistic conditions.
Medium
Limited loss, degraded functionality, or impact that depends on specific conditions.
Low
Minor impact, defence-in-depth gaps, or issues with unlikely preconditions.
Informational
Code quality, documentation or best-practice observations without direct security impact.

Limitations

  • Only the Aptos modules at the listed commit were reviewed in full; the Sui port was compared for parity of two fixes.
  • Off-chain reward reporting and validator delegation were out of scope.
  • MP-04 could not be proved because of prover timeouts on unbounded iteration.

No assessment can show that a system is free of vulnerabilities. This research assessment reflects the code at the commits listed. Quantix methodology

11

Technical appendix

Tools

  • Move ProverFormal specifications of accounting invariants
  • Aptos CLI test frameworkUnit and scenario tests
  • Manual reviewEntry-function and resource-flow analysis
Reviewed files (SHA-256) · 4 files
SHA-256 hashes of reviewed files
FileSHA-256
sources/staking_pool.movec6ced7bd910f9f4970192e1fabe718dc40eebbf3d81662e9028aaec1bf2d32b4
sources/rewards.movea1369cb508a2fe4f6017fb704811621a50fd01f2da2cdac1fdadcda192a6c820
sources/unstake_queue.moveb43a8d6cdb4f77bbb4b7e52ded70607916432f377a3ada993dcd7d72c5c4f9eb
sources/admin.move9b629bd43493f102e5864d262f83605ee5a8d858065044c126e70a6b5c13600b

Notes

  • The source repository is private; commit hashes identify the reviewed snapshot.

Technical references

  1. 1.Aptos Move book (opens in a new tab)
  2. 2.CWE-682: Incorrect Calculation (opens in a new tab)
  3. 3.CWE-841: Improper Enforcement of Behavioral Workflow (opens in a new tab)
  4. 4.Aptos gas and storage fees (opens in a new tab)
  5. 5.CWE-400: Uncontrolled Resource Consumption (opens in a new tab)
  6. 6.CWE-1339: Insufficient Precision or Accuracy of a Real Number (opens in a new tab)
  7. 7.Aptos events (opens in a new tab)

Quantix Technical Security Assessment QX-TSA-2026-006

Version 1.0, published 12 May 2026. A4 PDF generated from this assessment record.

Download Report

12

Report history

  1. 20 Apr 2026Review openedReviewed commit 00af372ccd08 in Private repository
  2. 29 Apr 2026Review closedTwo reviewers, eight working days
  3. After reviewRemediation reviewFixes checked at commit 8cd82e7b6c82
  4. 12 May 2026Version 1.0 publishedQX-TSA-2026-006, A4 PDF generated from this record

Commission an assessment written to be verified.

Share the repository and the commit you plan to deploy. We reply with scoping questions and an estimate.