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 wei first depositor plus a 1000 AVAX donation could steal about 500 AVAX from a later 2000 AVAX depositor, while smaller later deposits reverted with ZeroShares()
  • Summary: GoGoPool’s liquid-staking receipt token ggAVAX inherited classic ERC-4626 bootstrap risk but added a protocol-specific accelerator: direct WAVAX donations could be pulled into totalAssets() through syncRewards(), and the reward-cycle math let that synchronization happen almost immediately when called near a cycle boundary. If ggAVAX was created without meaningful seed liquidity, an attacker could front-run the first real deposit with 1 wei, donate WAVAX, call syncRewards() 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, a 2000 AVAX second depositor lost about 500 AVAX of value to the attacker’s opening share, and any depositor trying to deposit less than the donation amount reverted because previewDeposit() rounded to zero shares.
  • Trigger / entry point:
    • The vulnerable design centered on TokenggAVAX, which used an ERC-4626-style convertToShares() implementation inherited from ERC4626Upgradeable.
    • depositAVAX() reverted if previewDeposit(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 raw WAVAX balance already sitting in the vault start flowing into the accounting used by totalAssets().
    • Because syncRewards() rounded the next reward-cycle end to the next timestamp evenly divisible by rewardsCycleLength, an attacker could call it just before a boundary and compress the waiting time for reward recognition to roughly one second.
  • Root cause:
    • GoGoPool left ggAVAX bootstrap seeding to convention instead of enforcing meaningful initial liquidity inside vault creation.
    • The vault trusted raw donated WAVAX as 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.
  • Exploit path:
    1. The attacker watches for creation of a fresh ggAVAX vault with no meaningful seed liquidity.
    2. They call depositAVAX() with 1 wei and become the sole shareholder.
    3. They wrap a large amount of AVAX into WAVAX and transfer it directly to the ggAVAX contract without minting new shares.
    4. They call syncRewards() at a timestamp very close to the next rewardsCycleLength boundary so the donated WAVAX starts counting toward totalAssets() almost immediately.
    5. A real depositor arrives and mints against that inflated asset base even though the attacker still owns essentially all existing shares.
    6. In the public PoC, Alice deposited 2000 AVAX; Bob later withdrew 1500 AVAX despite only funding 1000 AVAX + 1 wei, extracting about 500 AVAX of Alice’s value.
    7. Depositors trying to deposit less than the donation amount reverted with ZeroShares() because the inflated exchange rate made previewDeposit() round down to zero.
  • What the attacker needed to be true:
    • ggAVAX needed to be unseeded or seeded too lightly when first made available.
    • Direct WAVAX transfers 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 table family, not yet a downstream lending-collateral exploit.
    • The protocol-specific twist was not just raw donation counts. It was raw donation counts through a public reward-sync path whose cycle rounding can collapse the waiting period to nearly zero.
    • depositAVAX()’s ZeroShares() 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 accrual code belongs inside the same solvency boundary as ERC-4626 share math when reward inventory can be attacker-supplied.
  • Representative code and artifacts:
    • Vulnerable ERC-4626 share conversion: contracts/contract/tokens/upgradeable/ERC4626Upgradeable.sol#L123 at commit aec9928d8bdce8a5a4efe45f54c39d4fc7313731
    • Vulnerable reward-sync path: contracts/contract/tokens/TokenggAVAX.sol#L102 at 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

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 wei first deposit, 1000 AVAX donation, 2000 AVAX later victim deposit, roughly 500 AVAX stolen by the opener, and ZeroShares() denial for deposits smaller than the donation amount.
  • The source also makes the timing accelerator concrete rather than theoretical. syncRewards() computes nextRewardsCycleEnd = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength, so at timestamp 1672876799 with a 14 day cycle the next reward boundary becomes 1672876800, 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 ETH as 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, not historical 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 ETH seed 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 rewards and 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 wei opening deposit could still own the entire economically meaningful share supply.
  • Reward-sync publication abuse: donated WAVAX became dangerous because syncRewards() let the attacker choose when it graduated into counted totalAssets().
  • 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 WAVAX donations were not inert ambient inventory; the protocol treated them as future reward inventory that syncRewards() 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 while totalSupply() 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 somewhere and existing shares already own it economically. Directly donated WAVAX should 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 into totalAssets(), 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 higher is 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 matters state, 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, or previewDeposit() 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 rewards can 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 day cadence collapsed into a one-second wait because 1672876799 rounded to the next cycle edge at 1672876800.
  • 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 visible and inventory became priced into shares, especially during the first live moments of a vault.

Internal linkages

Control surface