All incidents

Stars Arena Callback Weight Reentrancy

Share
Oct 07, 2023 05:23 UTCAttackLoss: 274,332.96 AVAXPending manual check1 exploit txWindow: Atomic
Estimated Impact
274,332.96 AVAX
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Oct 07, 2023 05:23 UTC → Oct 07, 2023 05:23 UTC

Exploit Transactions

TX 1Avalanche
0x4f37ffecdad598f53b8d5a2d9df98e3c00fbda4328585eb9947a412b5fe17ac5
Oct 07, 2023 05:23 UTCExplorer

Victim Addresses

0xa481b139a1a654ca19d2074f174f17d7534e8cecAvalanche
0x8af92c23a169b58c2e5ac656d8d8a23fc725080fAvalanche

Loss Breakdown

274,332.96AVAX

Similar Incidents

Root Cause Analysis

Stars Arena Callback Weight Reentrancy

1. Incident Overview TL;DR

On Avalanche block 36136406, exploit transaction 0x4f37ffecdad598f53b8d5a2d9df98e3c00fbda4328585eb9947a412b5fe17ac5 drained 274332.962 AVAX from the Stars Arena shares proxy at 0xa481b139a1a654ca19d2074f174f17d7534e8cec. The attacker EOA 0xa2ebf3fcd757e9be1e58b643b6b5077d11b4ad7a used a single transaction to deploy an orchestrator contract, deploy a helper subject contract, buy the first share of that helper subject, reenter Stars Arena during the subject-fee callback, overwrite the helper subject's pricing weights, then sell the only issued share back to the protocol at a catastrophically inflated price. The attacker EOA finished the transaction with a native-balance increase of 266095.85397 AVAX after gas.

The root cause is a state-ordering bug in the historical Stars Arena implementation 0x8af92c23a169b58c2e5ac656d8d8a23fc725080f. The first-buy path deferred the slot-0xa7 issuance guard until after attacker-controlled callbacks, while the weight-initialization function at selector 0x5632b2e4 treated that same slot as the proof that no shares had been issued yet. Because buyer balance and subject supply were already written to slots derived from bases 0xa8 and 0xa9 before the external subject-fee transfer, the callback executed in an inconsistent state: one share already existed, but the weight-initialization guard still read as open.

2. Key Background

Stars Arena exposed a proxy at 0xa481b139a1a654ca19d2074f174f17d7534e8cec that delegated to historical implementation 0x8af92c23a169b58c2e5ac656d8d8a23fc725080f for the exploit transaction. Three facts matter for this incident.

First, the implementation contained a public four-argument selector 0x5632b2e4 that initialized subject-specific pricing weights for msg.sender. The decompilation shows that it only checked two subject-local flags before writing the weight slots:

// Historical implementation decompilation
function Unresolved_5632b2e4(uint256 arg0, uint256 arg1, uint256 arg2, uint256 arg3) public {
    address var_a = msg.sender;
    require(!(bytes1(storage_map_f[var_a])), "Weights already initialized");
    var_a = msg.sender;
    require(!storage_map_f[var_a], "Can't change weights after shares have been issued");
    ...
    storage_map_f[var_a] = arg0;
    storage_map_f[var_a] = arg1;
    storage_map_f[var_a] = arg2;
    storage_map_f[var_a] = arg3;
    storage_map_f[var_a] = 0x01 | (uint248(storage_map_f[var_a]));
}

The corresponding disassembly for PCs 0x188c-0x1a99 shows this selector reading mapped slot bases 0xa1 and 0xa7, then writing mapped slots 0x9d, 0x9e, 0x9f, 0xa0, and finally setting the low bit in mapped slot 0xa1.

Second, both buy and sell pricing routed through internal routine 0x1a9b, which read the subject's weight configuration. That means a successful write to the four weight slots immediately changed the price observed by later buys and sells for that subject.

Third, the first-buy path executed external transfers before finalizing all issuance-sensitive bookkeeping. The disassembly for PCs 0x2058-0x2315 shows the buy path storing the buyer balance in the nested mapping derived from base slot 0xa8, storing the subject supply in the mapping derived from base slot 0xa9, then executing external transfer routines before slot 0xa7 is updated.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK case caused by callback-enabled state inconsistency. The intended invariant is straightforward: once any share of a subject exists, the subject's price-affecting weights must already be fixed and any later weight-initialization attempt must fail. Stars Arena did not enforce that invariant with the actual issuance state. Instead, it treated slot 0xa7 as the relevant no-shares-issued guard, even though the first-buy path had already written live issuance state into slots derived from 0xa8 and 0xa9.

That ordering error created a reentrancy window. During the first buy of an attacker-controlled subject contract, the protocol transferred the subject-fee payment to the subject before slot 0xa7 was updated. The subject contract therefore executed code while the protocol believed no issuance guard had been tripped, even though one share already existed in storage. The attacker used that callback to call selector 0x5632b2e4 and write the value 0x153005ce00 (91000000000) into all four pricing-weight slots. The later sellShares path reused those mutated weights through pricing routine 0x1a9b, producing a gross sell price of 274332968000000000000000 wei. This violates checks-effects-interactions, violates configuration immutability after issuance, and uses the wrong state variable as the reentrancy-sensitive guard.

4. Detailed Root Cause Analysis

The ACT opportunity existed in the Avalanche state immediately before execution of transaction 0x4f37ffecdad598f53b8d5a2d9df98e3c00fbda4328585eb9947a412b5fe17ac5 at block 36136406. At that point, the vulnerable proxy still delegated to the historical implementation, the helper subject's weight slots were uninitialized, the helper subject had zero supply, and the proxy held 276054972770000000000000 wei of native AVAX.

The invariant breach is visible directly in the victim disassembly. The weight-initialization selector uses slot 0xa1 as the "already initialized" flag and slot 0xa7 as the "shares already issued" flag. By contrast, the buy path updates balance and supply first, performs external calls second, and writes slot 0xa7 only after those calls return:

Historical implementation disassembly
0x2154-0x21a7  store buyer balance in mapping base 0xa8
0x21b2-0x21cd  store subject supply in mapping base 0xa9
0x220c-0x221d  external fee transfers via helper routines 0x307c and 0x30ef
0x2298-0x22cf  increment slot-0xa7 shareholder bookkeeping and store caller

That ordering makes the following execution sequence possible and, in the exploit transaction, actual:

  1. The attacker-created helper subject calls buySharesWithReferrer(helper, 1, helper) with 1 ether.
  2. The buy path writes the helper's share balance and total subject supply to one.
  3. Stars Arena pays the subject fee to the helper contract.
  4. The helper contract's fallback reenters the proxy and calls selector 0x5632b2e4 with four identical positive weights.
  5. The reentrant call succeeds because mapped slot 0xa7 for the helper subject is still zero.
  6. The buy path resumes, then finally updates slot 0xa7.
  7. The helper immediately calls sellShares(helper, 1), and the sell path prices that share using the attacker-written weights.

The exploit trace captures the critical callback and the exact storage writes:

Exploit transaction trace
0x8aF92C23...::buySharesWithReferrer{value: 1000000000000000000}(helper, 1, helper)
  helper::fallback{value: 420000000000000}()
    TransparentUpgradeableProxy::fallback(0x5632b2e4...)
      0x8aF92C23...::5632b2e4(...153005ce00, ...153005ce00, ...153005ce00, ...153005ce00)
        storage changes:
          @ ...b33bd2e: 0 -> 0x153005ce00
          @ ...ef0ea9:  0 -> 0x153005ce00
          @ ...a8d9ee:  0 -> 0x153005ce00
          @ ...e17a6d:  0 -> 0x153005ce00
          @ ...c395ea:  0 -> 1

The proxy-focused state diff records the same five writes after the transaction, which is the expected footprint for mapped slots 0x9d through 0xa1 for the helper subject. Those writes are not transient noise. They are the price inputs later consumed by the sell path.

The sell leg is equally explicit in the trace:

Exploit transaction trace
0x8aF92C23...::sellShares(helper, 1)
  helper::fallback{value: 246899671200000000000000}()
  protocolFeeDestination::fallback{value: 5486659360000000000000}()
  helper::fallback{value: 19203307760000000000000}()
  emit Trade(... false, 1, 274332968000000000000000, ...)

That Trade event is the semantic breakpoint where the incorrect pricing becomes economically real. The helper subject receives 246899671200000000000000 wei as seller proceeds and 19203307760000000000000 wei as the subject-fee payment, while the protocol fee destination receives 5486659360000000000000 wei and the referrer fee is 2743329680000000000000 wei. The helper later forwards 266103972780000000000000 wei to the attacker EOA, and the balance diff shows the attacker's end-of-transaction profit net of gas.

The exploit conditions are therefore concrete:

  • The attacker must control a subject contract that executes code on the subject-fee callback.
  • The subject's weight-initialization flag in mapped slot 0xa1 must still be clear.
  • Slot 0xa7 must still be unset when the callback executes.
  • The callback must write large positive weights into mapped slots 0x9d-0xa0.
  • The attacker must sell the issued share after the buy completes so the sell path observes the mutated weights.

5. Adversary Flow Analysis

The adversary cluster for this incident consists of:

  • EOA 0xa2ebf3fcd757e9be1e58b643b6b5077d11b4ad7a, which sent the exploit transaction and received final profit.
  • Orchestrator contract 0x7f283edc5ec7163de234e6a97fdfb16ff2d2c7ac, created inside the exploit transaction.
  • Helper subject contract 0xdd9afc0e3c43951659c8ebe7aef9ee40879863ea, also created inside the exploit transaction and used as the subject, callback surface, and immediate proceeds receiver.

Their end-to-end flow is:

  1. The attacker EOA deploys the orchestrator, which deploys the helper subject contract inside the same transaction.
  2. The helper subject buys one share of itself through buySharesWithReferrer.
  3. When the proxy pays the subject-fee transfer of 420000000000000 wei to the helper, the helper fallback reenters selector 0x5632b2e4 and stores 91000000000 into all four pricing-weight slots.
  4. The buy then finishes and records the helper as a shareholder by updating slot 0xa7, but that happens too late to prevent the weight overwrite.
  5. The helper calls sellShares(helper, 1), which now prices one share using the attacker-selected weights and emits a sell Trade event with gross price 274332968000000000000000 wei.
  6. The helper forwards its entire balance to the attacker EOA.

The balance diff quantifies the final transfers:

{
  "attacker_before_wei": "105601135480982496047",
  "attacker_after_wei": "266201455105480982496047",
  "attacker_delta_wei": "266095853970000000000000",
  "proxy_before_wei": "276054972770000000000000",
  "proxy_after_wei": "1722010770000000000000",
  "proxy_delta_wei": "-274332962000000000000000"
}

The attacker used no privileged role, no compromised key, and no off-chain secret. This is a permissionless one-transaction exploit enabled by public contract entrypoints and attacker-controlled callback execution.

6. Impact & Losses

The direct victim was the Stars Arena shares proxy at 0xa481b139a1a654ca19d2074f174f17d7534e8cec. The historical implementation at 0x8af92c23a169b58c2e5ac656d8d8a23fc725080f was the vulnerable logic target during the exploit transaction.

Measured loss is:

  • AVAX: raw amount "274332962000000000000000" with 18 decimals.

The proxy's native balance fell from 276054.97277 AVAX to 1722.01077 AVAX in a single transaction. The attacker EOA's native balance increased from 105.601135480982496047 AVAX to 266201.455105480982496047 AVAX, for a net increase of 266095.85397 AVAX after gas. The remaining extracted value was distributed to fee recipients exactly as recorded in the sell Trade event.

7. References

  1. Exploit transaction 0x4f37ffecdad598f53b8d5a2d9df98e3c00fbda4328585eb9947a412b5fe17ac5 on Avalanche block 36136406.
  2. Seed metadata for the exploit transaction, including sender, input, value, and block context.
  3. Opcode-level trace for the exploit transaction showing the helper deployment, buySharesWithReferrer, reentrant 0x5632b2e4 call, and sellShares.
  4. Proxy-focused state diff showing the post-transaction writes for the helper subject's mapped slots 0x9d-0xa1.
  5. Trade logs emitted by the proxy for the buy and sell events, including the inflated sell price.
  6. Native balance diff for the exploit transaction, confirming attacker profit and proxy depletion.
  7. Decompiled and disassembled historical implementation 0x8af92c23a169b58c2e5ac656d8d8a23fc725080f, especially selector 0x5632b2e4, pricing routine 0x1a9b, the buy path at PCs 0x2058-0x2315, and the sell path at PCs 0x2805-0x2aea.