Stars Arena Reentrancy (2023)
Smart contract security audit by TierZero · 2024-02-15
Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.
Findings summary
Independent research — not a commissioned audit. This is a post-mortem compiled from public reports and on-chain data. TierZero was not engaged by Stars Arena and has not reviewed any non-public source.
What the protocol did
Stars Arena was a SocialFi application launched on Avalanche C-Chain in September 2023 — a bonding-curve "friend.tech" clone. Users bought and sold "shares" (tickets) tied to a specific creator's address, with price moving along a curve as supply changed, and every buy or sell settling in AVAX. Each trade took a protocol fee and a creator fee out of the payout, with the remainder priced off the curve and the pool's current "weight" (the running value used to compute both price and payout for that subject's shares). Roughly three weeks after launch, on October 7, 2023, the shares contract was exploited for a sum widely reported at approximately $3 million in AVAX.
The vulnerability
The bug is textbook reentrancy: a checks-effects-interactions violation in the sell-share path. The sell function computed the AVAX payout for the shares being sold, sent that AVAX to the caller via a low-level call, and only afterward updated the state that tracked the caller's share balance and the pool's weight. Because a native-asset transfer to a contract address executes that contract's receive/fallback function before control returns to the caller, an attacker whose "wallet" was itself a contract could re-enter the sell function from inside that fallback — before their balance or the pool's weight had been decremented from the first call.
Illustrative pseudocode of the flawed pattern:
function sellShares(address subject, uint256 amount) external {
uint256 price = getSellPrice(subject, amount);
uint256 payout = price - getFees(price);
// INTERACTION happens first
(bool sent, ) = msg.sender.call{value: payout}("");
require(sent, "transfer failed");
// EFFECTS happen after — too late
sharesBalance[subject][msg.sender] -= amount;
sharesSupply[subject] -= amount;
weight[subject] -= amount; // pricing/reward weight updated last
}
Any state read during pricing (getSellPrice, weight) is still stale on re-entry, so the same shares can be sold repeatedly against a curve that hasn't yet reflected the prior sale.
How the exploit worked
- Attacker deploys a small contract whose
receive()/fallback calls back intosellShares. - Attacker takes a position in a creator's shares.
- Attacker's contract calls
sellShares. The contract computes the payout and sends AVAX via a low-level call before touching storage. - The AVAX transfer triggers the attacker's fallback, which immediately calls
sellSharesagain for the same position —sharesBalanceandweightstill reflect pre-sale state. - Each nested call recomputes a payout against the stale weight and pays out again for shares the attacker no longer economically holds.
- Recursion continues until a depth or gas limit is hit, or the contract's AVAX balance runs low, unwinding cleanly and leaving the attacker with far more AVAX than their position was worth.
- The reported outflow was on the order of $3M in AVAX. Stars Arena paused the contract, and the team subsequently negotiated the return of a large share of the funds, treating a portion as an informal bounty rather than pursuing full recovery through other means.
How an audit catches this
This is one of the highest-yield checks in any bonding-curve, vault, or share-accounting review, and it's largely mechanical:
- Flag every function that both sends native value or tokens externally and mutates balance/accounting state, and check whether the external call precedes the state write.
- Enforce checks-effects-interactions on every payable withdrawal path — not just the obvious ones, but buy/sell curves, staking withdrawals, and reward claims alike.
- Write a reentrancy proof-of-concept: a malicious receiver that re-enters the function under test from
receive(), asserting the nested call either reverts or is blocked by a guard. - Add reentrancy as an explicit invariant, not a stylistic note: no state-mutating function should leave global weight/supply inconsistent with what all outstanding share balances imply after execution.
- Run static analysis (Slither's reentrancy detectors) and manually trace every function containing
call{value:},.transfer, or.sendfollowed by storage writes.
This is the same accounting-state-vs-external-call ordering class we look for in any curve or AMM logic, and it's a standing checklist item in every /services/smart-contract-audit engagement — the same discipline that catches account-validation-ordering bugs in CPI-heavy Solana programs, discussed in our piece on /blog/cross-program-invocation-risk-solana-defi.
Remediation
- Reorder the function: compute the payout, update
sharesBalance,sharesSupply, andweightfirst, then perform the external value transfer last. - Add a reentrancy guard (
nonReentrantmodifier) on every function sending native value or calling into an arbitrary address, as defense in depth even after fixing ordering. - Prefer a pull-payment pattern over
call{value:}()to arbitrary addresses where feasible — letting users withdraw their own balance removes most of the reentrancy surface for payouts entirely. - Where a curve prices against a shared pool weight, snapshot the pre-trade weight at function entry and validate post-trade state against it, so a nested call can't operate on inconsistent numbers.
- Treat sell/withdraw paths as the top-priority target on any bonding-curve fork of this design — the transfer-before-effects pattern recurs across friend.tech-style clones on every EVM chain, and the equivalent failure mode on Solana is account/state validation ordering rather than reentrancy in the classic sense, covered in our /blog/solana-program-security-audit-checklist-trading.
Reentrancy from a value-transfer-before-state-update pattern is one of the oldest bug classes in this industry and one of the cheapest to catch before mainnet. If your contract has a sell, withdraw, or claim function that moves native value, get the ordering checked before it ships — that's the core of what /services/smart-contract-audit does.
Thinking about a review like this for your own contracts? Get a smart contract audit from an independent team.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit