This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0xd88f26f2f9145fa413db0cfd5d3eb121e3a50a3fdcee16c9bd4731e68332ce4b0x7bacb1c805cbbf7c4f74556a4b34fde7793d0887BSC0x3d11015d9044cabbb2504448e37f20d0d56e36f8BSCOn 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.
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 .
delegatecallThe 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.
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.
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.
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.
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:
admin = 0x5366....setImplementation(address) is guarded only by onlyAdmin, the now-attacker-controlled admin immediately upgrades the proxy to attacker code at the same address.The historical post-state reads confirm the takeover:
post
admin = 0x5366c6BA729D9cF8d472500aFc1A2976aC2fE9fF
implementation = 0x5366c6BA729D9cF8d472500aFc1A2976aC2fE9fF
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:
transferFrom against users who had previously approved the vault as spender,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:
-224922137071076982-1288000000000000000000+1288224922137071076982The 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:
initialize(address,address,uint64) selector to the implementation,_defaultExpireDuration,admin retained unrestricted implementation-upgrade authority through setImplementation(address).The violated security principles were equally direct:
The attacker flow is a single-transaction ACT sequence.
0x8ebd046992afe07eacce6b9b3878fdb45830f42b0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff0x7bacb1c805cbbf7c4f74556a4b34fde7793d08870x3D11015d9044cAbBB2504448e37f20d0d56E36F8The attacker-submitted transaction was:
chainid = 56)328209520xd88f26f2f9145fa413db0cfd5d3eb121e3a50a3fdcee16c9bd4731e68332ce4b0x8ebd046992afe07eacce6b9b3878fdb45830f42bto: 0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ffThe helper contract orchestrated three stages inside the same transaction:
Proxy reinitialization:
RewardVaultDelegator.initialize(0x5366..., 0x5366..., 1).RewardVault.initialize and rewrites the proxy's privileged slots.Unauthorized but now-authorized implementation change:
RewardVaultDelegator.setImplementation(0x5366...).NewImplementation(0x3D11015d9044cAbBB2504448e37f20d0d56E36F8, 0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff).Delegatecalled theft and profit realization:
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.
The exploit caused both governance compromise and direct asset loss.
Governance and control impact:
Measured asset losses from the collected balance diff and the auditor's loss summary:
37335415.530122063120663838 (37335415530122063120663838 raw, 18 decimals)1288.224922137071076982 (1288224922137071076982 raw, 18 decimals)12341371.593503137 (12341371593503137 raw, 9 decimals)3945.033718231379966543 (3945033718231379966543 raw, 18 decimals)18882.727542566235544252 (18882727542566235544252 raw, 18 decimals)3318.622570906921282235 (3318622570906921282235 raw, 18 decimals)Native-profit realization for the attacker:
0.096019436637.2299436619533228290.002739422537.133924225353322829The 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.
Primary transaction and state evidence:
0xd88f26f2f9145fa413db0cfd5d3eb121e3a50a3fdcee16c9bd4731e68332ce4bNewImplementation and downstream token-transfer logsinitialize, setImplementation, and subsequent delegatecalled theft actionsPrimary victim code evidence:
RewardVault.initialize(address,address,uint64) in the verified RewardVault implementationRewardVaultDelegator.setImplementation(address) in the verified proxy contractDelegatorInterface.fallback() and _fallback() showing unrestricted delegatecall forwardingAddresses of interest:
0x7bacb1c805cbbf7c4f74556a4b34fde7793d08870x3D11015d9044cAbBB2504448e37f20d0d56E36F80xE9547CF7E592F83C5141bB50648317e35D27D29B0x59558B7D604675A4CC9EB36c67f159550F73Bf9A0x8ebd046992afe07eacce6b9b3878fdb45830f42b0x5366c6ba729d9cf8d472500afc1a2976ac2fe9ff