Calculated from recorded token losses using historical USD prices at the incident time.
0xbc452fdc8f851d7c5b72e1fe74dfb63bb793d511EthereumHedgey ClaimCampaigns at 0xbc452fdc8f851d7c5b72e1fe74dfb63bb793d511 was drained of 1,303,910.12 USDC after an attacker deployed helper contract 0xc793113f1548b97e37c409f39244ee44241bf2b3, used a Balancer flash loan in tx 0xa17fdb804728f226fcd10e78eae5247abd984e0f03301312315b89cae25aa517 to create and immediately cancel a fake locked campaign, and then consumed the leftover USDC allowance in tx 0x2606d459a50ca4920722a111745c2eeced1d8a01ff25ee762e22d5d4b1595739.
The root cause is that createLockedCampaign grants ERC20 allowance from the shared ClaimCampaigns custody balance to an arbitrary claimLockup.tokenLocker, while cancelCampaign removes campaign bookkeeping but does not revoke that approval. Because the approval is attached to the whole ClaimCampaigns USDC balance rather than campaign-scoped accounting, the attacker could bootstrap approval with flash-loaned funds and later spend unrelated claimant funds already held by the contract.
Hedgey ClaimCampaigns is a shared-custody contract: multiple campaigns rely on the same ERC20 balance held by the single ClaimCampaigns address. That design means an allowance emitted by ClaimCampaigns affects the entire token balance at the contract, not only the campaign that triggered the approval.
Balancer Vault flash loans are permissionless. Any unprivileged actor can borrow assets, execute arbitrary callback logic, and repay within one transaction. That property lets an attacker temporarily fund a fake campaign without needing permanent upfront capital.
ERC20 allowances persist in token storage until explicitly changed. Deleting internal campaign structs inside ClaimCampaigns does not revoke an allowance already written in the USDC contract. This persistence is the technical reason the fake campaign setup remained exploitable after cancellation.
This is an ATTACK-class vulnerability caused by overbroad delegated spend authority from a shared custody contract. The violated invariant is: campaign setup and teardown must never grant any external contract a spend right over unrelated claimant funds held in ClaimCampaigns custody.
The critical breakpoint is in ClaimCampaigns createLockedCampaign, where the contract increases ERC20 allowance from ClaimCampaigns itself to an arbitrary claimLockup.tokenLocker. In this incident, the attacker chose its own helper contract as tokenLocker. The setup transaction moved flash-loaned USDC into ClaimCampaigns, wrote the helper allowance, then canceled the campaign and returned only the flash-loaned amount. ClaimCampaigns therefore ended the transaction with its original claimant USDC balance intact and with a live helper allowance large enough to drain it. The cancellation path cleaned up campaign storage but not the external approval, so the later withdrawal transaction simply consumed the stale approval with transferFrom.
Representative execution evidence from the setup trace:
ClaimCampaigns::createLockedCampaign(... token: USDC, amount: 1305000000000, tokenLocker: helper ...)
FiatTokenV2_2::approve(helper, 1305000000000) // owner = ClaimCampaigns
ClaimCampaigns::cancelCampaign(0x00000000000000000000000000000000)
FiatTokenV2_2::transfer(helper, 1305000000000) // returns fake campaign amount
The setup receipt independently confirms the approval event on USDC:
Approval(
owner = 0xbc452fdc8f851d7c5b72e1fe74dfb63bb793d511,
spender = 0xc793113f1548b97e37c409f39244ee44241bf2b3,
value = 1305000000000
)
The adversary cluster consists of EOA 0xded2b1a426e1b7d415a40bcad44e98f47181dda2 and helper contract 0xc793113f1548b97e37c409f39244ee44241bf2b3. Deployment metadata shows the EOA deployed the helper in tx 0xf6dfdd6b152d9bf03ff296ddc980757c198e8bff0c85f949bba1f533ca493828, passing ClaimCampaigns 0xbc452fdc8f851d7c5b72e1fe74dfb63bb793d511 and Balancer Vault 0xba12222222228d8ba445958a75a0704d566bf2c8 as constructor arguments.
Before the setup transaction, ClaimCampaigns already held 1,303,910.12 USDC (1303910120000 raw units). The setup trace at block 19687887 shows the helper receiving a Balancer flash loan of 1305000000000 USDC, approving ClaimCampaigns to pull it, and then calling createLockedCampaign with:
manager = helper
token = USDC
amount = 1305000000000
tokenLockup = Locked
claimLockup.tokenLocker = helper
donation.tokenLocker = helper
Inside that call, ClaimCampaigns first pulled the flash-loaned USDC from the helper, raising its balance from 1303910120000 to 2608910120000. It then wrote a USDC allowance from ClaimCampaigns to the helper for 1305000000000. The allowance is global at the USDC token layer; it is not scoped to the specific campaign id.
The helper then immediately called cancelCampaign(bytes16(0)). The trace shows ClaimCampaigns transferring 1305000000000 USDC back to the helper and deleting the campaign storage slots. What it does not do is reset the USDC allowance that had been written during setup. After cancellation and flash-loan repayment, ClaimCampaigns still held the original 1303910120000 USDC, and the helper still had authority to spend up to 1305000000000 from ClaimCampaigns.
That leftover approval converted a temporary flash-loan-funded setup into a durable spend right over unrelated claimant funds. Three blocks later, in tx 0x2606d459a50ca4920722a111745c2eeced1d8a01ff25ee762e22d5d4b1595739, the attacker called the helper’s withdrawal function with USDC and amount = 0, which the helper interprets as “drain the full ClaimCampaigns balance.” The seed transaction metadata shows the attacker EOA sending the call directly to the helper, and the trace/balance evidence ties the outcome to a full transfer of the remaining ClaimCampaigns USDC balance to the attacker cluster.
The ACT exploit conditions are also deterministic:
claimLockup.tokenLocker.campaign.amount; a permissionless Balancer flash loan satisfies that requirement.The attack unfolded in three on-chain stages.
First, the attacker deployed helper contract 0xc793113f1548b97e37c409f39244ee44241bf2b3 from EOA 0xded2b1a426e1b7d415a40bcad44e98f47181dda2 in tx 0xf6dfdd6b152d9bf03ff296ddc980757c198e8bff0c85f949bba1f533ca493828. The helper was an attacker-owned orchestrator that could receive flash loans, create and cancel campaigns, and later call transferFrom.
Second, the attacker executed the allowance-bootstrap transaction 0xa17fdb804728f226fcd10e78eae5247abd984e0f03301312315b89cae25aa517 in block 19687887. The helper borrowed 1305000000000 USDC from Balancer Vault, created a fake locked campaign that named itself as both manager and token locker, caused ClaimCampaigns to approve it, canceled the campaign, and repaid the flash loan. The setup trace summary captures the essential state transition:
Balancer Vault transfers 1305000000000 USDC to helper.
Helper causes ClaimCampaigns to approve helper for 1305000000000 USDC.
ClaimCampaigns transfers 1305000000000 USDC back to helper.
Helper repays Balancer Vault.
Third, the attacker realized the gain in tx 0x2606d459a50ca4920722a111745c2eeced1d8a01ff25ee762e22d5d4b1595739 in block 19687890. The transaction target is the helper contract, and the calldata selector matches its withdrawal path. The helper then invoked USDC transferFrom(ClaimCampaigns, attackerEOA, USDC.balanceOf(ClaimCampaigns)), draining the remaining ClaimCampaigns balance into the attacker EOA.
This is an ACT opportunity because no privileged access was required. Any unprivileged actor could deploy an equivalent helper, borrow from Balancer’s public flash-loan entrypoint, choose itself as token locker, and later consume the stale approval.
ClaimCampaigns lost the entire pre-existing USDC balance it held on behalf of legitimate claim campaigns. The measured loss is:
USDC: 1303910120000 raw units = 1,303,910.12 USDC
The effect was not limited to the fake campaign created by the attacker. Because the approval was granted from the shared ClaimCampaigns custody address, the drain consumed unrelated claimant funds already parked in the contract. After the drain, legitimate claimants faced a zero-USDC balance at the custody contract for that asset.
0xbc452fdc8f851d7c5b72e1fe74dfb63bb793d5110xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb480xba12222222228d8ba445958a75a0704d566bf2c80xf6dfdd6b152d9bf03ff296ddc980757c198e8bff0c85f949bba1f533ca4938280xa17fdb804728f226fcd10e78eae5247abd984e0f03301312315b89cae25aa5170x2606d459a50ca4920722a111745c2eeced1d8a01ff25ee762e22d5d4b1595739root_cause.json