All incidents

RewardVault Proxy Reinitialization Theft

Share
Oct 22, 2023 08:26 UTCAttackLoss: 37,335,415.53 RACA, 1,288.22 BUSD +4 morePending manual check1 exploit txWindow: Atomic
Estimated Impact
37,335,415.53 RACA, 1,288.22 BUSD +4 more
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Oct 22, 2023 08:26 UTC → Oct 22, 2023 08:26 UTC

Exploit Transactions

TX 1BSC
0xd88f26f2f9145fa413db0cfd5d3eb121e3a50a3fdcee16c9bd4731e68332ce4b
Oct 22, 2023 08:26 UTCExplorer

Victim Addresses

0x7bacb1c805cbbf7c4f74556a4b34fde7793d0887BSC
0x3d11015d9044cabbb2504448e37f20d0d56e36f8BSC

Loss Breakdown

37,335,415.53RACA
1,288.22BUSD
12,341,371.59FLOKI
3,945.03OLE
18,882.73CSIX
3,318.62BABY

Similar Incidents

Root Cause Analysis

RewardVault Proxy Reinitialization Theft

1. Incident Overview TL;DR

On BNB Smart Chain block 32820952, transaction 0xd88f26f2f9145fa413db0cfd5d3eb121e3a50a3fdcee16c9bd4731e68332ce4b let an unprivileged attacker take over RewardVaultDelegator at 0x7bacb1c805cbbf7c4f74556a4b34fde7793d0887, upgrade it to attacker-controlled code, and drain both vault-held assets and third-party user allowances in a single transaction. The attacker EOA was 0x8ebd046992afe07eacce6b9b3878fdb45830f42b; the attacker-operated helper contract was 0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff.

The root cause is a classic upgradeable-contract initialization failure. RewardVault.initialize(address,address,uint64) remained publicly reachable through the proxy fallback and had no one-time initialization guard, so any caller could rewrite the proxy's admin, distributor, and defaultExpireDuration storage. After seizing admin, the attacker legitimately invoked RewardVaultDelegator.setImplementation(address) and then executed arbitrary delegatecalled theft logic.

2. Key Background

RewardVaultDelegator is a proxy-style contract whose storage holds the live implementation address and the admin address. Any function selector that the proxy does not implement itself is forwarded to the current implementation through fallback() and delegatecall.

The deployed implementation was RewardVault at 0x3D11015d9044cAbBB2504448e37f20d0d56E36F8. Its state variables include admin, defaultExpireDuration, and distributor, which means any delegated call to the implementation mutates proxy storage rather than implementation storage.

That design is only safe if the initializer is reachable exactly once during deployment. The constructor of RewardVaultDelegator performs an initialization delegatecall and then sets implementation, but the runtime proxy still exposes the same implementation ABI to arbitrary callers through fallback. If the implementation initializer is left public and unguarded, post-deployment callers can repeat the initialization and overwrite privileged proxy state.

The vault also held custodial ERC-20 balances and had standing ERC-20 allowances from some users. Once the proxy admin was corrupted and the implementation was swapped, the proxy became a general-purpose attacker execution surface with access to both categories of value.

3. Vulnerability Analysis & Root Cause Summary

The incident is an ATTACK, not a pure MEV opportunity. The protocol invariant that only the authorized admin flow may change proxy control-plane state was broken by a public initializer that wrote directly into proxy storage when called through fallback. The vulnerable implementation code is:

function initialize(address payable _admin, address _distributor, uint64 _defaultExpireDuration) public {
    require(_defaultExpireDuration > 0, "Incorrect inputs");
    admin = _admin;
    distributor = _distributor;
    defaultExpireDuration = _defaultExpireDuration;
}

The proxy-side reachability is:

fallback() external payable {
    _fallback();
}

function _fallback() internal {
    if (msg.data.length > 0) {
        (bool success, ) = implementation.delegatecall(msg.data);
        ...
    }
}

And the privileged upgrade step that becomes exploitable after the admin overwrite is:

function setImplementation(address implementation_) public override onlyAdmin {
    address oldImplementation = implementation;
    implementation = implementation_;
    emit NewImplementation(oldImplementation, implementation);
}

The enabling breakpoint is therefore precise: after deployment, initialize(address,address,uint64) should no longer be callable in proxy context by arbitrary users, but it remained callable and rewrote admin without any initialized-state check.

4. Detailed Root Cause Analysis

4.1 Code-Level Mechanism

The verified RewardVault source shows the initializer at the start of the contract, with no initialized flag, constructor-only restriction, or onlyAdmin gate:

function initialize(address payable _admin, address _distributor, uint64 _defaultExpireDuration) public {
    require(_defaultExpireDuration > 0, "Incorrect inputs");
    admin = _admin;
    distributor = _distributor;
    defaultExpireDuration = _defaultExpireDuration;
}

The verified proxy support code shows that all unknown selectors are forwarded to the current implementation:

fallback() external payable {
    _fallback();
}

function _fallback() internal {
    if (msg.data.length > 0) {
        (bool success, ) = implementation.delegatecall(msg.data);
        ...
    }
}

This means that a runtime call to RewardVaultDelegator.initialize(...) does not execute in the implementation's own storage. It executes in proxy storage, so writes to admin, distributor, and defaultExpireDuration mutate the live proxy authority slots.

4.2 Observed Pre-State

Immediately before the exploit, the historical reads collected for the proxy show:

pre
admin                 = 0xE9547CF7E592F83C5141bB50648317e35D27D29B
implementation        = 0x3D11015d9044cAbBB2504448e37f20d0d56E36F8
distributor           = 0x59558B7D604675A4CC9EB36c67f159550F73Bf9A
defaultExpireDuration = 7776000

The same pre-state also included vault-held reward tokens and existing ERC-20 approvals from third-party users to the vault. In particular, the balance diff for the exploit transaction shows the proxy held 1288 BUSD units that were later fully drained, and approved user 0xe83c6e8feedde85e72e810f82ee0943aa14ed2f6 exposed another 224.922137071076982 BUSD through a pre-existing allowance to the proxy.

4.3 Breakpoint in the Exploit Trace

The seed trace excerpt records the exact takeover sequence:

0x7bAC...::initialize(0x5366..., 0x5366..., 1)
  0x3D11...::initialize(...) [delegatecall]
    storage changes:
      @ 1:  0x...e9547cf7e592f83c5141bb50648317e35d27d29b
         -> 0x...5366c6ba729d9cf8d472500afc1a2976ac2fe9ff
      @ 10: 0x0000000059558b7d604675a4cc9eb36c67f159550f73bf9a000000000076a700
         -> 0x000000005366c6ba729d9cf8d472500afc1a2976ac2fe9ff0000000000000001

0x7bAC...::setImplementation(0x5366...)
  storage changes:
    @ 0: 0x...3d11015d9044cabbb2504448e37f20d0d56e36f8
       -> 0x...5366c6ba729d9cf8d472500afc1a2976ac2fe9ff

Those two writes fully explain the privilege escalation:

  1. The attacker re-runs the public initializer through the proxy, setting admin = 0x5366....
  2. Because setImplementation(address) is guarded only by onlyAdmin, the now-attacker-controlled admin immediately upgrades the proxy to attacker code at the same address.
  3. All subsequent proxy calls now delegate into attacker-controlled logic.

The historical post-state reads confirm the takeover:

post
admin          = 0x5366c6BA729D9cF8d472500aFc1A2976aC2fE9fF
implementation = 0x5366c6BA729D9cF8d472500aFc1A2976aC2fE9fF

4.4 Why the Theft Was Then Possible

Once the proxy pointed to attacker code, the attacker no longer needed to bypass any additional authorization. The proxy itself became the execution identity for arbitrary delegated logic, which let the attacker:

  • approve routers on behalf of the vault,
  • transfer tokens already held by the vault,
  • call transferFrom against users who had previously approved the vault as spender,
  • swap stolen assets into WBNB/BNB, and
  • forward realized proceeds to the attacker EOA.

The receipt and trace evidence show exactly that pattern. For example, the BUSD leg includes an allowance read on the approved user, a transferFrom from that user into the proxy, and subsequent draining of the vault's own BUSD balance. The balance diff quantifies the same outcome:

  • Approved user BUSD delta: -224922137071076982
  • RewardVault BUSD delta: -1288000000000000000000
  • BUSD captured downstream: +1288224922137071076982

The native-balance deltas show the attacker EOA rising from 0.0960194366 BNB to 37.229943661953322829 BNB, for a net increase of 37.133924225353322829 BNB after 0.0027394225 BNB in gas.

The flaw is therefore not in PancakeRouter or any downstream token contract. Those systems behaved normally after the attacker had already obtained unauthorized proxy admin and arbitrary upgrade authority. The root cause remains the victim-side missing initializer guard combined with proxy fallback reachability.

The ACT exploit conditions were straightforward and public:

  • the proxy still forwarded the initialize(address,address,uint64) selector to the implementation,
  • the implementation still accepted any caller and any positive _defaultExpireDuration,
  • and the current admin retained unrestricted implementation-upgrade authority through setImplementation(address).

The violated security principles were equally direct:

  • initialization of upgradeable logic must be one-time,
  • privileged proxy storage must never be writable through public delegated entrypoints,
  • and a contract that holds assets or user approvals must never permit arbitrary post-deployment code execution.

5. Adversary Flow Analysis

The attacker flow is a single-transaction ACT sequence.

5.1 Actors

  • Attacker EOA: 0x8ebd046992afe07eacce6b9b3878fdb45830f42b
  • Attacker helper / malicious implementation: 0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff
  • Victim proxy: 0x7bacb1c805cbbf7c4f74556a4b34fde7793d0887
  • Legitimate implementation before takeover: 0x3D11015d9044cAbBB2504448e37f20d0d56E36F8

5.2 Transaction Sequence

The attacker-submitted transaction was:

  • Chain: BNB Smart Chain (chainid = 56)
  • Block: 32820952
  • Transaction hash: 0xd88f26f2f9145fa413db0cfd5d3eb121e3a50a3fdcee16c9bd4731e68332ce4b
  • Sender: 0x8ebd046992afe07eacce6b9b3878fdb45830f42b
  • to: 0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff

The helper contract orchestrated three stages inside the same transaction:

  1. Proxy reinitialization:

    • Calls RewardVaultDelegator.initialize(0x5366..., 0x5366..., 1).
    • This delegatecalls into RewardVault.initialize and rewrites the proxy's privileged slots.
  2. Unauthorized but now-authorized implementation change:

    • Calls RewardVaultDelegator.setImplementation(0x5366...).
    • Emits NewImplementation(0x3D11015d9044cAbBB2504448e37f20d0d56E36F8, 0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff).
  3. Delegatecalled theft and profit realization:

    • Executes attacker-defined logic through the proxy.
    • Drains vault-held tokens.
    • Uses existing user approvals to pull more tokens through the proxy identity.
    • Swaps stolen assets into WBNB/BNB and forwards proceeds to the attacker EOA.

The critical decision point is after stage 1. Once admin is overwritten, every later step is simply the normal capability of the admin role or the arbitrary capability of the newly installed implementation.

6. Impact & Losses

The exploit caused both governance compromise and direct asset loss.

Governance and control impact:

  • Full unauthorized takeover of the proxy admin role.
  • Unauthorized replacement of the production implementation with attacker code.
  • Permanent loss of trust in all proxy-held allowances and custodied assets at the time of exploitation.

Measured asset losses from the collected balance diff and the auditor's loss summary:

  • RACA: 37335415.530122063120663838 (37335415530122063120663838 raw, 18 decimals)
  • BUSD: 1288.224922137071076982 (1288224922137071076982 raw, 18 decimals)
  • FLOKI: 12341371.593503137 (12341371593503137 raw, 9 decimals)
  • OLE: 3945.033718231379966543 (3945033718231379966543 raw, 18 decimals)
  • CSIX: 18882.727542566235544252 (18882727542566235544252 raw, 18 decimals)
  • BABY: 3318.622570906921282235 (3318622570906921282235 raw, 18 decimals)

Native-profit realization for the attacker:

  • BNB before: 0.0960194366
  • BNB after: 37.229943661953322829
  • Gas paid: 0.0027394225
  • Net BNB increase: 37.133924225353322829

The incident also proves that third-party users who had approved the vault as spender were exposed, even when their assets were not physically held inside the vault at the start of the transaction.

7. References

Primary transaction and state evidence:

  • Exploit transaction: 0xd88f26f2f9145fa413db0cfd5d3eb121e3a50a3fdcee16c9bd4731e68332ce4b
  • Seed metadata for the transaction, including sender, gas, and block context
  • Seed balance diff showing native and ERC-20 deltas
  • Receipt showing NewImplementation and downstream token-transfer logs
  • Trace excerpt showing initialize, setImplementation, and subsequent delegatecalled theft actions
  • Historical pre/post reads confirming admin and implementation changes

Primary victim code evidence:

  • RewardVault.initialize(address,address,uint64) in the verified RewardVault implementation
  • RewardVaultDelegator.setImplementation(address) in the verified proxy contract
  • DelegatorInterface.fallback() and _fallback() showing unrestricted delegatecall forwarding

Addresses of interest:

  • Victim proxy: 0x7bacb1c805cbbf7c4f74556a4b34fde7793d0887
  • Victim implementation: 0x3D11015d9044cAbBB2504448e37f20d0d56E36F8
  • Original admin before exploit: 0xE9547CF7E592F83C5141bB50648317e35D27D29B
  • Original distributor before exploit: 0x59558B7D604675A4CC9EB36c67f159550F73Bf9A
  • Attacker EOA: 0x8ebd046992afe07eacce6b9b3878fdb45830f42b
  • Attacker helper / malicious implementation: 0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff