On Base at 2026-04-29 02:22:41 UTC, the Syndicate Commons bridge proxy at 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f was drained via a privileged upgrade compromise. The loss in the analyzed transaction was 18,445,374.987536122947149447 SYND (about 18.445M SYND, reported publicly as roughly $330k). This transaction was the final drain step, not the initial compromise: control of the bridge’s upgrade path had already been seized in earlier transactions.

Incident Summary

This incident is best understood as a three-stage attack rather than as a bug in a normal bridge function.

First, an attacker-controlled path gained control over the bridge’s upgrade admin. Next, the attacker used that new authority to replace the legitimate bridge implementation with a malicious implementation. Finally, the attacker called the newly installed drain entrypoint, which caused the proxy to transfer its entire SYND balance to the attacker EOA.

The key point is that the last drain transaction only worked because the bridge proxy had already been repointed to attacker-controlled logic. The original bridge flow was no longer what the proxy was executing at exploit time.

Attack Narrative

Stage 1 - Seize Upgrade Control

The control-plane compromise happened in transaction 0xd92ba704c491ff0a085558d965fc078b66dbaf9ad1c8ce3588b4a8c5e37e0b1a at block 45319998.

The caller 0x61e667cb3128ac161cd68033639dfbb496e30950 sent a transaction to UpgradeExecutor proxy 0xc924178de522feb6147df2668e44fe7683852cee using selector 0xbca8c7b5, which resolves to executeCall(address,bytes). The calldata instructed UpgradeExecutor to call transferOwnership(address) on ProxyAdmin 0x7b0ab8f15940f573c1d82c2238cfa8368edbf9c6, setting the new owner to attacker EOA 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e.

This transaction succeeded with status 0x1 and emitted an OwnershipTransferred(address,address) event on ProxyAdmin, proving that ownership moved from 0xc924178de522feb6147df2668e44fe7683852cee to 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e.

Operationally, this was the decisive step: once the attacker controlled ProxyAdmin, they controlled which implementation the bridge proxy would execute.

Why Stage 1 Was Possible

This stage was not the attacker directly calling ProxyAdmin as its owner. Instead, the attacker path used an address that already had permission to invoke UpgradeExecutor.executeCall(address,bytes), and UpgradeExecutor itself was the owner of ProxyAdmin immediately before the takeover.

The on-chain logic therefore worked as follows:

  1. 0x61e667... called UpgradeExecutor.executeCall(target, data).
  2. UpgradeExecutor accepted that call because the caller had EXECUTOR_ROLE.
  3. UpgradeExecutor then performed an external call to ProxyAdmin.
  4. ProxyAdmin saw msg.sender == 0xc924..., which was its legitimate owner at that time.
  5. As a result, transferOwnership(0x58e2...) succeeded and handed upgrade control to the attacker.

This is the critical architectural point: the address that submitted the transaction did not need to be the owner of ProxyAdmin. It only needed to be able to drive UpgradeExecutor, because UpgradeExecutor was already sitting in the owner position over ProxyAdmin.

What the Contract Problem Appears To Be

From the chain evidence alone, this first stage does not look like a classic Solidity bug such as a missing onlyOwner, reentrancy, or arithmetic flaw. Instead, it looks like a high-privilege execution path being too powerful.

UpgradeExecutor.executeCall(address,bytes) is guarded by onlyRole(EXECUTOR_ROLE), but once that check passes it can make an arbitrary call to an arbitrary target with arbitrary calldata. In this deployment, that mattered because ProxyAdmin ownership was held by UpgradeExecutor. The result is that compromise or misuse of an EXECUTOR_ROLE address effectively becomes compromise of the upgrade admin itself.

In practical terms, the dangerous combination was:

  • EXECUTOR_ROLE could trigger unrestricted executeCall
  • ProxyAdmin.owner() was UpgradeExecutor
  • ProxyAdmin controls which implementation the bridge proxy executes

That means the executor role was not just an operational helper role. It was, indirectly, a path to full upgrade authority over the bridge.

Stage 2 - Replace the Bridge Logic

Four blocks later, in transaction 0x48b36cd9d50348e2fb0082e9af6abebf1c4c9ef1d268923ce5ef79c4dc613178 at block 45320003, attacker EOA 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e called ProxyAdmin 0x7b0ab8f15940f573c1d82c2238cfa8368edbf9c6 with selector 0x99a88ec4, which resolves to upgrade(address,address).

That call upgraded CommonsBridgeProxy 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f to implementation 0xd5e2237968e36a1e6ec11bdea8e04bcf821247f8. The transaction succeeded and the proxy emitted Upgraded(address), confirming that the new implementation was active.

From this point onward, calls to the bridge proxy no longer executed the previous bridge implementation. They executed the attacker’s implementation instead.

Stage 3 - Drain the SYND Balance

The actual drain happened in transaction 0xb4aca0c74ec2526631771c9f2d1936e8ba12264d41b2ed188f145c52bcd475dc at block 45320007.

In this transaction, attacker EOA 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e called the bridge proxy 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f with a 4-byte selector only: 0x5b8ec19f.

The proxy forwarded the call to implementation 0xd5e2237968e36a1e6ec11bdea8e04bcf821247f8 via DELEGATECALL. Because this was a delegatecall, the implementation ran in the storage and balance context of the proxy. In practice, that means address(this) inside the malicious implementation referred to the bridge proxy, which was the account actually holding the bridged SYND balance.

The recovered implementation behavior and the trace align on three concrete steps:

  1. It only allowed calls from attacker EOA 0x58e2....
  2. It queried SYND.balanceOf(address(this)), where address(this) was the proxy.
  3. It transferred that entire balance to 0x58e2....

So the drain was not a partial withdrawal and did not depend on a forged bridge message in this final transaction. It was a direct balance sweep after the proxy had already been turned into attacker-controlled code.

Why the Final Transaction Works

The final drain transaction only makes sense together with the earlier admin-takeover and upgrade transactions.

If the attacker had called selector 0x5b8ec19f against the original bridge implementation, the call would not have produced this balance sweep. The reason it succeeded is that the proxy had already been upgraded to malicious logic. In other words, the exploitable event was the loss of upgrade control; the drain selector was simply the attacker’s final cash-out step.

Detailed Call Trace

The following call flow is derived directly from trace_callTracer.json for transaction 0xb4aca0c74ec2526631771c9f2d1936e8ba12264d41b2ed188f145c52bcd475dc:

  1. 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e -> 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f
    • CALL
    • selector 0x5b8ec19f
    • no ETH value
  2. 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f -> 0xd5e2237968e36a1e6ec11bdea8e04bcf821247f8
    • DELEGATECALL
    • selector 0x5b8ec19f
    • execution context remains the proxy, so address(this) is still 0x64d4...
  3. 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f -> 0x11dc28d01984079b7efe7763b533e6ed9e3722b9
    • STATICCALL
    • balanceOf(address) / selector 0x70a08231
    • argument: 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f
    • return value: 0x0000000000000000000000000000000000000000000f41f5c81cd2fefd599e87
  4. 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f -> 0x11dc28d01984079b7efe7763b533e6ed9e3722b9
    • CALL
    • transfer(address,uint256) / selector 0xa9059cbb
    • recipient: 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e
    • amount: 0x0f41f5c81cd2fefd599e87 = 18,445,374.987536122947149447 SYND

Financial Impact

The bridge proxy lost 18,445,374.987536122947149447 SYND, and the attacker EOA gained the exact same amount in the same transaction according to funds_flow.json. The sole token transfer in the receipt moved the entire SYND balance from 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f to 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e, so the proxy’s custodial SYND inventory was fully drained in this step. Public incident reporting and the follow-on liquidation activity cited in the incident brief put the realized value at roughly $330k.

The direct losers were the funds custodied by the Commons bridge proxy, which functionally means the bridge treasury or bridged-asset pool rather than a single per-user balance in this transaction. This report does not show other tokens being drained from the same proxy in the analyzed transaction, but the compromised upgrade path would have put any assets held by the proxy at risk until access was revoked.

Evidence

  • UpgradeExecutor.executeCall(address,bytes) exists at artifacts/analysis_0xb4aca0c74ec2526631771c9f2d1936e8ba12264d41b2ed188f145c52bcd475dc/0x1273bad06599b67722b4f94334e957d2c2d3a0f7/src/UpgradeExecutor.sol:73
  • hasRole(EXECUTOR_ROLE, 0x61e667cb3128ac161cd68033639dfbb496e30950) == true immediately before the takeover transaction, confirming the caller was already an authorized executor
  • ProxyAdmin.owner() was 0xc924178de522feb6147df2668e44fe7683852cee immediately before the takeover and 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e immediately after
  • OwnershipTransferred(address,address) on ProxyAdmin 0x7b0ab8f15940f573c1d82c2238cfa8368edbf9c6 in transaction 0xd92ba704c491ff0a085558d965fc078b66dbaf9ad1c8ce3588b4a8c5e37e0b1a moved ownership from 0xc924178de522feb6147df2668e44fe7683852cee to 0x58e2e08357992bc4a4a02f22321c3e0f6b01893e
  • Upgraded(address) on CommonsBridgeProxy 0x64d426e64b7191a1fcf48c89d2d12772fbfb297f in transaction 0x48b36cd9d50348e2fb0082e9af6abebf1c4c9ef1d268923ce5ef79c4dc613178 set implementation 0xd5e2237968e36a1e6ec11bdea8e04bcf821247f8
  • The recovered malicious implementation only exposes the unresolved selector 0x5b8ec19f and implements the sweep behavior observed in the trace; see artifacts/analysis_0xb4aca0c74ec2526631771c9f2d1936e8ba12264d41b2ed188f145c52bcd475dc/0xd5e2237968e36a1e6ec11bdea8e04bcf821247f8/recovered.sol:16
  • The drain transaction receipt succeeded with status 0x1 and emitted a single relevant Transfer log at logIndex 232 moving 18,445,374.987536122947149447 SYND from the proxy to the attacker
  • Selector checks: 0xbca8c7b5 = executeCall(address,bytes), 0xf2fde38b = transferOwnership(address), 0x99a88ec4 = upgrade(address,address), 0x70a08231 = balanceOf(address), 0xa9059cbb = transfer(address,uint256); selector 0x5b8ec19f remains unresolved by public signature databases