GoGoPool ggAVAX First-Depositor Share-Price Inflation Near Miss (2023-01-03)
- Name: GoGoPool ggAVAX first-depositor share-price inflation near miss
- Date: 2023-01-03
- Protocol: GoGoPool
ggAVAX - Chain: Avalanche C-Chain
- Category: first-deposit inflation / ERC-4626 donation attack / reward-sync accounting abuse / zero-share victimization / whitehat near miss
- Loss: no realized theft; Code4rena’s public proof of concept showed that a
1 weifirst depositor plus a1000 AVAXdonation could steal about500 AVAXfrom a later2000 AVAXdepositor, while smaller later deposits reverted withZeroShares() - Summary: GoGoPool’s liquid-staking receipt token
ggAVAXinherited classic ERC-4626 bootstrap risk but added a protocol-specific accelerator: directWAVAXdonations could be pulled intototalAssets()throughsyncRewards(), and the reward-cycle math let that synchronization happen almost immediately when called near a cycle boundary. IfggAVAXwas created without meaningful seed liquidity, an attacker could front-run the first real deposit with1 wei, donateWAVAX, callsyncRewards()so the vault treated the donation as rewards while the attacker still owned essentially all shares, then let later users mint against the inflated exchange rate. In Code4rena’s proof of concept, a2000 AVAXsecond depositor lost about500 AVAXof value to the attacker’s opening share, and any depositor trying to deposit less than the donation amount reverted becausepreviewDeposit()rounded to zero shares. - Trigger / entry point:
- The vulnerable design centered on
TokenggAVAX, which used an ERC-4626-styleconvertToShares()implementation inherited fromERC4626Upgradeable. depositAVAX()reverted ifpreviewDeposit(assets) == 0, so the protocol did not silently mint zero shares; instead, small later deposits became impossible once the share price was inflated far enough.syncRewards()let rawWAVAXbalance already sitting in the vault start flowing into the accounting used bytotalAssets().- Because
syncRewards()rounded the next reward-cycle end to the next timestamp evenly divisible byrewardsCycleLength, an attacker could call it just before a boundary and compress the waiting time for reward recognition to roughly one second.
- The vulnerable design centered on
- Root cause:
- GoGoPool left
ggAVAXbootstrap seeding to convention instead of enforcing meaningful initial liquidity inside vault creation. - The vault trusted raw donated
WAVAXas future reward inventory and exposed a public synchronization path that could make that inventory economically count almost immediately. - Share minting still used the standard
supply == 0 ? assets : assets * totalSupply / totalAssets()shape, so once the attacker owned the whole opening supply, every donated asset pushed the next depositor onto a worse exchange rate. - The protocol therefore combined two otherwise survivable choices into one exploit surface: an unseeded ERC-4626 vault and a reward-sync path that could rapidly internalize unsolicited assets.
- GoGoPool left
- Exploit path:
- The attacker watches for creation of a fresh
ggAVAXvault with no meaningful seed liquidity. - They call
depositAVAX()with1 weiand become the sole shareholder. - They wrap a large amount of AVAX into
WAVAXand transfer it directly to theggAVAXcontract without minting new shares. - They call
syncRewards()at a timestamp very close to the nextrewardsCycleLengthboundary so the donatedWAVAXstarts counting towardtotalAssets()almost immediately. - A real depositor arrives and mints against that inflated asset base even though the attacker still owns essentially all existing shares.
- In the public PoC, Alice deposited
2000 AVAX; Bob later withdrew1500 AVAXdespite only funding1000 AVAX + 1 wei, extracting about500 AVAXof Alice’s value. - Depositors trying to deposit less than the donation amount reverted with
ZeroShares()because the inflated exchange rate madepreviewDeposit()round down to zero.
- The attacker watches for creation of a fresh
- What the attacker needed to be true:
ggAVAXneeded to be unseeded or seeded too lightly when first made available.- Direct
WAVAXtransfers to the vault had to remain economically visible to reward accounting. syncRewards()needed to be callable by the attacker and able to move the donation into the effective asset base quickly enough to front-run later deposits.- The attacker needed later honest deposits large enough to make the opening donation profitable.
- Technical anatomy worth remembering:
- This was the pure
next depositor funds the first depositor's capitalization tablefamily, not yet a downstream lending-collateral exploit. - The protocol-specific twist was not just
raw donation counts. It wasraw donation counts through a public reward-sync path whose cycle rounding can collapse the waiting period to nearly zero. depositAVAX()’sZeroShares()guard did not solve the problem. It merely changed one tail of the attack from silent theft into denial of service for smaller depositors while still allowing larger depositors to be overcharged.- The case is also a useful reminder that
reward accrualcode belongs inside the same solvency boundary as ERC-4626 share math when reward inventory can be attacker-supplied.
- This was the pure
- Representative code and artifacts:
- Vulnerable ERC-4626 share conversion:
contracts/contract/tokens/upgradeable/ERC4626Upgradeable.sol#L123at commitaec9928d8bdce8a5a4efe45f54c39d4fc7313731 - Vulnerable reward-sync path:
contracts/contract/tokens/TokenggAVAX.sol#L102at the same commit - Guarded-but-still-exploitable deposit path:
contracts/contract/tokens/TokenggAVAX.sol#L166 - Public Foundry proof of concept:
test/unit/TokenggAVAX.t.sol#L108 - Mitigation PR:
multisig-labs/gogopool#49
- Vulnerable ERC-4626 share conversion:
Source confidence / evidence split
- This incident is preserved as an audit-stage near miss rather than a live exploit, so the highest-confidence evidence is the Code4rena finding plus the Foundry PoC bundled into the public issue. There is no production attack trace to reconstruct around mempool timing or realized cash-out routes.
- The primary issue report gives the most useful quantitative skeleton:
1 weifirst deposit,1000 AVAXdonation,2000 AVAXlater victim deposit, roughly500 AVAXstolen by the opener, andZeroShares()denial for deposits smaller than the donation amount. - The source also makes the timing accelerator concrete rather than theoretical.
syncRewards()computesnextRewardsCycleEnd = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength, so at timestamp1672876799with a14 daycycle the next reward boundary becomes1672876800, only one second later. - The mitigation-review issue is narrower but important: it confirms GoGoPool accepted the bootstrap-inflation finding and fixed it by having the deployer seed the vault during deployment, with the reviewer explicitly describing
1 ETHas the implemented initial amount. - Because this note comes from disclosed audit artifacts instead of chain forensics, the healthiest way to read it is
validated exploitability and validated mitigation shape, nothistorical theft timeline. - Response / mitigation:
- The issue was disclosed during Code4rena’s December 2022–January 2023 GoGoPool review and rated high severity.
- GoGoPool mitigated it before production exposure by changing deployment so the deployer seeds initial liquidity; the mitigation review notes mention an initial
1 ETHseed as the fix shape. - The mitigation is worth preserving because it solved the opening-state exploit economically, not by claiming the reward-sync path itself was harmless in an empty vault.
- Key transaction hashes:
- No public theft transaction hashes exist because this was preserved as an audit-stage near miss rather than a live drain.
- The key public artifact is the Foundry proof of concept bundled with the Code4rena finding.
Why this incident matters
- It is a clean, protocol-specific first-deposit inflation exemplar rather than only a standards-level thought experiment.
- It shows how a vault can remain exploitable even when
deposit()reverts on zero shares: larger depositors can still be victimized while smaller users are simply locked out. - It also sharpens a reusable design lesson for liquid-staking and reward-bearing wrappers: if attackers can donate future
rewardsand also choose when those rewards become priced into shares, reward accounting is part of vault bootstrap security.
Pattern tags:
- First-depositor cap-table capture: a
1 weiopening deposit could still own the entire economically meaningful share supply. - Reward-sync publication abuse: donated
WAVAXbecame dangerous becausesyncRewards()let the attacker choose when it graduated into countedtotalAssets(). - Bootstrap-state donation inflation: the printed share price targeted later depositors directly rather than needing a downstream lending market.
- Zero-share denial tail: once the exchange rate was inflated far enough, smaller honest deposits stopped being victimized quietly and instead reverted with
ZeroShares(). - Design companions: first-deposit-inflation, sync-harvest-and-cache-write-authority-discipline, and bootstrap-state-irreversibility-and-supply-floor-design.
Why the exploit worked
- GoGoPool left the opening supply state too weak, so the first depositor could cheaply control the capitalization table for all later share issuance.
- Direct
WAVAXdonations were not inert ambient inventory; the protocol treated them as future reward inventory thatsyncRewards()could pull into economic share value. syncRewards()timing mattered because reward-cycle rounding let the attacker compress the wait for recognition to almost nothing if they called near a cycle boundary.- Standard ERC-4626 share math then did the rest: once
totalAssets()reflected the donation whiletotalSupply()still belonged to the attacker, later users minted against an attacker-authored exchange rate. - The
ZeroShares()guard only changed the tail behavior. It prevented silent zero-share mints for some small depositors but did not stop large later depositors from subsidizing the attacker’s opening share.
Detection and audit pivots
- Enforce non-burnable seed liquidity or virtual offsets at vault creation, and verify that no later path can restore effectively-unseeded economics.
- Treat public reward-sync, harvest, and checkpoint functions as security-critical publication surfaces. Ask who can call them and how fast attacker-supplied inventory becomes counted assets.
- Fuzz deposits around cycle boundaries and reward-recognition boundaries, not only at arbitrary timestamps. Timing compression can be the exploit multiplier.
- Test both victim tails after share-price inflation: silent overcharging of large later depositors and denial of service for smaller deposits via
previewDeposit() == 0. - Ask whether donated reward inventory, staking proceeds, or externally transferred assets can become canonical
totalAssets()before any independent check proves they belong to existing shares.
Failure-prevention checklist
- Seed vaults inside initialization, not by later convention. GoGoPool’s own mitigation path is the clean reminder that meaningful non-burnable opening liquidity has to exist before a public first depositor can race for the whole capitalization table.
- Preserve a hard distinction between
reward inventory exists somewhereandexisting shares already own it economically. Directly donatedWAVAXshould not become next-depositor pricing truth merely because a public helper can see it. - Treat
syncRewards()-style helpers as publication authority, not harmless bookkeeping. If public callers can choose when pending or attacker-supplied inventory graduates intototalAssets(), they partially control issuance terms for the next depositor. - Keep bootstrap protection and reward publication protection as one joined review item. An unseeded vault and an attacker-timed reward-recognition path are each dangerous alone, but together they create a direct depositor-theft rail.
- Bound or delay reward recognition around cycle edges. If rounding or calendar logic can compress a nominal multi-day wait into seconds, the protocol should assume attackers will optimize for that boundary.
- Test the full victim surface after inflation: large later deposits that still mint overpriced shares, and smaller deposits that revert because
previewDeposit()rounds to zero.ZeroShares()is not a safety proof if it only changes theft into selective denial of service. - Require an explicit answer for unsolicited balance growth: ignore it, quarantine it, socialize it slowly, or only recognize it after a stronger provenance check.
Raw balance is higheris not enough by itself. - Re-run first-depositor tests after any upgrade that changes reward timing,
totalAssets()composition, or synchronization helpers. Vault bootstrap safety can be re-broken by seemingly unrelated reward-accounting features.
Audit prompts
- What exact event proves this new vault is past its dangerous
first depositor owns everything that mattersstate, and can any later burn, migration, or drain recreate that state? - Can direct transfers, donated rewards, validator proceeds, or other off-path inventory reach
totalAssets()without minting proportional shares or passing through an explicit recognition policy? - Who can call
syncRewards()or equivalent helpers, and what is the shortest realistic time from attacker donation to canonical share-pricing truth? - Does cycle rounding, epoch rollover, or timestamp alignment let a nominally slow reward path become nearly immediate when called at a specific boundary?
- If the share price is attacker-inflated, what happens to both tails of the next-depositor experience: overpayment for large deposits and
ZeroShares()denial for smaller ones? - Which code path decides that donated or pending reward inventory belongs to existing shareholders rather than to a quarantined bucket, and what evidence does that path rely on besides a raw balance increase?
- Could the vault be safe if rewards were observable but not yet published into
totalAssets(), or if the vault were seeded but reward publication stayed public and attacker-timed? Which half of the combined exploit surface is actually carrying the risk? - What telemetry would show the attack starting in production: first depositor owns nearly all shares, raw vault balance jumps without matching share issuance,
syncRewards()gets called near a cycle edge, orpreviewDeposit()starts returning zero for ordinary deposit sizes?
Lessons that should already be standard
- Bootstrap safety and reward accounting are not separate subsystems when attacker-supplied
rewardscan set the next share price. ZeroShares()is not a defense against first-deposit inflation. It only means some victims are rejected instead of quietly overcharged.- Public synchronization timing is economic authority. Whoever can choose when pending inventory becomes counted assets can often choose who gets diluted or overcharged next.
- Time-based smoothing is only as strong as its shortest reachable path. In GoGoPool’s public PoC, the nominal
14 daycadence collapsed into a one-second wait because1672876799rounded to the next cycle edge at1672876800. - Seed liquidity is a governance and deployment invariant, not an operational nice-to-have. If the protocol needs a meaningful first balance to stay safe, that balance must be enforced at creation time.
- Direct donations are not benign just because they are economically real. The protocol still has to decide whether, when, and for whom they count.
- Reward-bearing wrappers should assume attackers will test the exact seam between
inventory became visibleandinventory became priced into shares, especially during the first live moments of a vault.
Internal linkages
- Core recurring pattern: first-deposit-inflation.
- Parent family once the same exchange-rate lie crosses into downstream solvency rails: donation-share-price-manipulation.
- Publication-authority pattern: state-publication-authority-failure — the danger became real because
syncRewards()let the attacker choose when donatedWAVAXstopped being ambient inventory and started being canonical share-value truth. - Publication-authority companion: sync-harvest-and-cache-write-authority-discipline —
syncRewards()was not harmless housekeeping; it was the authority surface that decided when attacker-donated inventory graduated into live share-pricing truth. - Reward / presentation companion: reward-recognition-presentation-and-collateral-separation — the deeper question is why donated
WAVAXwas allowed to graduate from visible reward inventory into holder-owned share value quickly enough to victimize the next depositor. - Bootstrap companion: bootstrap-state-irreversibility-and-supply-floor-design — the healthier launch-state answer is to make the opening capitalization table expensive to capture instead of relying on convention.
- Adjacent launch-state pattern: fresh-market-initialization-failure — same
first actor defines the opening statelesson, but usually monetized through lending-market setup rather than direct vault depositor theft.
Control surface
-
The decisive leverage sat in three places at once: the unseeded
ggAVAXbootstrap state, the ERC-4626 share-mint formula that let the first depositor own the whole capitalization table, andsyncRewards()turning attacker-donatedWAVAXinto counted assets on attacker-chosen timing. -
The dangerous protocol promise was not simply
this is an ERC-4626 vault. It wasthe same contract can decide who owns the opening shares and when donated assets start looking like earned rewards. -
Sources:
-
Last reviewed: 2026-08-01 UTC