All incidents

CPIMP Proxy Takeover

Share
Feb 24, 2026 00:55 UTCAttackLoss: Pending manual check1 exploit txWindow: Atomic
Estimated Impact
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Feb 24, 2026 00:55 UTC → Feb 24, 2026 00:55 UTC

Exploit Transactions

TX 1BSC
0x6a79beb44d1ccde2d822b3f6636aebbfbc754630f9b48407fdde05bb2f96f60a
Feb 24, 2026 00:55 UTCExplorer

Victim Addresses

0xAf15eFD95a762949b3F6d4AC4e28Af2d50bEc3e0BSC
0x6b85e5377feb17d356fd39e57af2b62edf454173BSC

Similar Incidents

Root Cause Analysis

CPIMP Proxy Takeover

1. Incident Overview TL;DR

On BSC block 82,997,149, transaction 0x6a79beb44d1ccde2d822b3f6636aebbfbc754630f9b48407fdde05bb2f96f60a let an unprivileged adversary take control of proxy 0xAf15eFD95a762949b3F6d4AC4e28Af2d50bEc3e0, the NewBCT / CPIMP proxy. The sender 0x3dA435fc3331b87B68576334367D017F95247aeC used Multicall3 at 0xcA11bde05977b3631167028862bE2a173976CA11 to invoke helper contract 0xb383f87db018638191cadddf9833a9ab124d9146, which in turn called the victim proxy.

The critical sequence was deterministic. First, the helper called setFeeCollector(address) through the proxy and wrote attacker-controlled state. Second, it called upgradeToAndCall(address,bytes) through the same proxy and installed attacker implementation 0x70c76350bbfce3acc56ee18cbfb3ab312626323e. Although the receipt emitted a second Upgraded event back to the old implementation, the final state diff shows the ERC1967 implementation slot still points to the attacker implementation at transaction end.

The root cause is unrestricted access to privileged implementation functions exposed through an ERC1967 proxy. The victim implementation 0x6b85e5377feb17d356fd39e57af2b62edf454173 allows any caller to execute both setFeeCollector(address) and upgradeToAndCall(address,bytes) without owner, admin, or role authorization.

2. Key Background

The victim contract at 0xAf15eFD95a762949b3F6d4AC4e28Af2d50bEc3e0 is a standard OpenZeppelin ERC1967 proxy. The verified proxy source shows that the active implementation is stored at slot:

bytes32 internal constant IMPLEMENTATION_SLOT =
    0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

That matters because any implementation method reached through the proxy executes in proxy storage. A privileged write in implementation code therefore mutates live protocol state.

The original implementation 0x6b85e5377feb17d356fd39e57af2b62edf454173 is unverified, so the analysis relies on two concrete evidence sources: the Heimdall decompilation and the observed attack trace. The decompilation is sufficient here because the relevant selectors and storage effects are simple and are confirmed by the trace and final state diff.

The seed transaction 0x8e04b7c1b90f113562413dfee1cf8b449defe6602fb140d6ebc08eb59bb433cc is only a warning message transaction. It does not participate in the exploit path and does not affect the takeover predicate.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK-category ACT takeover, not a speculative or partial finding. The victim proxy remained pointed at a vulnerable implementation that exposed privileged control-plane functions to any external caller. Because ERC1967 proxies delegate execution into the implementation while preserving proxy storage, those public functions were effectively public write access to the proxy's control state.

The first vulnerable breakpoint is selector 0xa42dce80, identified in the decompilation as setFeeCollector(address). Its body performs a direct storage write and contains no access-control branch:

function setFeeCollector(address arg0) public payable returns (uint256) {
    require(msg.value);
    require((0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc + msg.data.length) < 0x20);
    require(arg0 - (address(arg0)));
    store_l = (uint96(store_l)) | (address(arg0));
    return ;
}

The second vulnerable breakpoint is selector 0x4f1ef286, the UUPS upgrade entrypoint. The decompilation shows proxy-context checks and a proxiableUUID() compatibility check, but no owner/admin/role validation before writing the new implementation and delegatecalling attacker-supplied initialization data:

function Unresolved_4f1ef286(address arg0, uint256 arg1) public payable {
    ...
    (bool success, bytes memory ret0) = address(arg0).proxiableUUID(); // staticcall
    ...
    store_a = (address(arg0)) | (uint96(store_a));
    emit Upgraded(address(arg0));
    ...
    (bool success, bytes memory ret0) = address(arg0).Unresolved_(var_g); // delegatecall
    ...
}

The violated invariant is straightforward: only authorized governance or upgrade operators should be able to mutate sensitive configuration and change the ERC1967 implementation slot. Instead, interface compatibility was checked, but caller authorization was not.

4. Detailed Root Cause Analysis

The ACT pre-state is the BSC chain state immediately before block 82997149, when the proxy still pointed to implementation 0x6b85e5377feb17d356fd39e57af2b62edf454173. The prestate diff confirms the implementation slot held the original implementation and that the attacker-controlled storage base slot 0xf364fa666b2e082663ca7dd04c16a2c736d1990df80fd97015fe242a48f33a00 was unset:

{
  "0x360894...82bbc": "0x0000000000000000000000006b85e5377feb17d356fd39e57af2b62edf454173"
}

The attack begins when EOA 0x3dA435fc3331b87B68576334367D017F95247aeC submits a normal type-2 transaction to Multicall3. The trace shows the downstream call into helper 0xb383f87db018638191cadddf9833a9ab124d9146, and then into the victim proxy:

SM Address: 0xb383f87db018638191cadddf9833a9ab124d9146, caller:0xca11...ca11,target:0xb383...9146
SM Address: 0xaf15efd95a762949b3f6d4ac4e28af2d50bec3e0, caller:0xb383...9146,target:0xaf15...c3e0 is_static:false, input_size:36
SM Address: 0xaf15efd95a762949b3f6d4ac4e28af2d50bec3e0, caller:0xb383...9146,target:0xaf15...c3e0 is_static:false, input_size:100

The verbose trace also captures the calldata shape embedded in the helper payload. It contains selector 0xa42dce80 for setFeeCollector(address) and then performs upgradeToAndCall with attacker implementation 0x70c76350bbfce3acc56ee18cbfb3ab312626323e. Near the execution breakpoint, the trace records:

0x6b85E5377feb17D356FD39E57AF2B62Edf454173::upgradeToAndCall(0x70c76350BbfCE3aCC56EE18cbFb3aB312626323e, 0x) [delegatecall]
0x70c76350BbfCE3aCC56EE18cbFb3aB312626323e::proxiableUUID() [staticcall]
emit Upgraded(implementation: 0x70c76350BbfCE3aCC56EE18cbFb3aB312626323e)
@ 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc:
  0x0000000000000000000000006b85e5377feb17d356fd39e57af2b62edf454173
  -> 0x00000000000000000000000070c76350bbfce3acc56ee18cbfb3ab312626323e
@ 0xf364fa666b2e082663ca7dd04c16a2c736d1990df80fd97015fe242a48f33a00:
  0 -> 0x000000000000000000000000b383f87db018638191cadddf9833a9ab124d9146

The receipt contains two Upgraded(address) logs, first to the attacker implementation and then back to the original implementation:

[
  {
    "address": "0xaf15efd95a762949b3f6d4ac4e28af2d50bec3e0",
    "topics": ["0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", "0x00000000000000000000000070c76350bbfce3acc56ee18cbfb3ab312626323e"]
  },
  {
    "address": "0xaf15efd95a762949b3f6d4ac4e28af2d50bec3e0",
    "topics": ["0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", "0x0000000000000000000000006b85e5377feb17d356fd39e57af2b62edf454173"]
  }
]

Those logs alone are misleading. The prestate-tracer state diff is the authoritative post-state artifact and shows that the live implementation slot remained on 0x70c76350bbfce3acc56ee18cbfb3ab312626323e after the transaction completed. That same diff shows the attacker-controlled slot root populated with helper-controlled values, proving persistent compromise of proxy control state.

This is therefore a deterministic ACT opportunity. The adversary needed only a regular EOA, a UUPS-compatible malicious implementation, and the public on-chain victim contracts. No privileged key, no protocol-owned signer, and no attacker-only artifact was required.

5. Adversary Flow Analysis

The adversary cluster consists of three directly evidenced addresses:

  • 0x3dA435fc3331b87B68576334367D017F95247aeC: the EOA that submitted the takeover transaction.
  • 0xb383f87db018638191cadddf9833a9ab124d9146: the helper/orchestrator contract called from Multicall3.
  • 0x70c76350bbfce3acc56ee18cbfb3ab312626323e: the malicious implementation installed during the upgrade.

The exploit flow is:

  1. The attacker EOA submits tx 0x6a79beb44d1ccde2d822b3f6636aebbfbc754630f9b48407fdde05bb2f96f60a to Multicall3.
  2. Multicall3 calls helper 0xb383...9146.
  3. The helper calls the proxy path for setFeeCollector(address) and writes attacker-controlled state through proxy storage.
  4. The helper immediately calls upgradeToAndCall(address,bytes) on the same proxy.
  5. The victim implementation performs only UUPS compatibility and proxy-context checks, writes the ERC1967 implementation slot, emits Upgraded, and delegatecalls the attacker implementation.
  6. The attacker-controlled storage base slot 0xf364...3a00 is populated with attacker helper address 0xb383...9146, leaving persistent control data in the proxy.

The execution is visible in the transaction trace and the resulting storage diff. There is no evidence of failed authorization checks because none exist on the relevant implementation paths.

6. Impact & Losses

No immediate token loss was realized in the observed transaction. The collected balance diff shows only gas expenditure by the sender and corresponding native-coin movement to the block producer. The impact is non-monetary but severe: the attacker obtained persistent control over the victim proxy implementation and attacker-defined control storage.

That control-plane compromise means future user interactions with the proxy can be routed through attacker logic. In practical terms, the adversary gained the ability to replace protocol behavior at the proxy address without any legitimate governance action.

7. References

  1. Transaction 0x6a79beb44d1ccde2d822b3f6636aebbfbc754630f9b48407fdde05bb2f96f60a trace, showing helper-mediated calls into setFeeCollector(address) and upgradeToAndCall(address,bytes).
  2. Transaction 0x6a79beb44d1ccde2d822b3f6636aebbfbc754630f9b48407fdde05bb2f96f60a receipt, showing both Upgraded(address) logs.
  3. Transaction 0x6a79beb44d1ccde2d822b3f6636aebbfbc754630f9b48407fdde05bb2f96f60a prestate-tracer diff, showing the final ERC1967 implementation slot and attacker-controlled storage writes.
  4. Verified OpenZeppelin ERC1967Proxy.sol source for proxy 0xAf15eFD95a762949b3F6d4AC4e28Af2d50bEc3e0.
  5. Heimdall decompilation of implementation 0x6b85e5377feb17d356fd39e57af2b62edf454173, specifically selector 0xa42dce80 (setFeeCollector(address)) and selector 0x4f1ef286 (upgradeToAndCall(address,bytes)).