ACP Router/PaymentManager Double-Claim Drain on Base
Exploit Transactions
0xe94a5ed54d0a9aa317c997607d7d1ea9828ad47626d7794b0e4020ff49cdf9a0Victim Addresses
0xef4364fe4487353df46eb7c811d4fac78b856c7fBase0xa6c9ba866992cfd7fd6460ba912bfa405ada9df0BaseLoss Breakdown
Similar Incidents
Veil01ETH forged-proof drain on Base
33%Base DUCKVADER infinite mint + Uniswap drain
32%Base USDC drain from malicious transferFrom spender approvals
31%MPRO Staking Proxy unwrapWETH Flash-Loan Exploit (Base)
31%SynapLogicErc20 Router Flash-Loan Over-Mint Exploit
30%USDC drain via unchecked Uniswap V3-style callback
29%Root Cause Analysis
ACP Router/PaymentManager Double-Claim Drain on Base
1. Incident Overview TL;DR
In Base tx 0xe94a5ed54d0a9aa317c997607d7d1ea9828ad47626d7794b0e4020ff49cdf9a0 (block 42832267), an unprivileged adversary created an attacker-controlled ACP job, progressed it to COMPLETED, and caused the same budget to be released twice. The second release drained pooled USDC from ACP PaymentManager beyond the job’s escrow.
Root cause is the combination of two historical implementation flaws active at the incident block:
PaymentManagerimplementation0x56c3af6c5995147f293dc756216920fd24d50684had escrow sufficiency check disabled inreleasePayment.ACPRouterimplementation0x307e34d9421e63d2ca92fab68ae720304927d6e8allowed non-idempotent payout invocation through_claimBudgetand explicitclaimBudget(jobId).
Observed outcomes in the same transaction were deterministic: payment vault net -97,000 USDC, attacker EOA +58,200 USDC, and treasury +38,800 USDC.
2. Key Background
ACP uses proxy modules. At pre-state σ_B (before block 42832267, i.e., B=42832266), incident-relevant proxies pointed to historical implementations:
Router proxy 0xa6c9ba866992cfd7fd6460ba912bfa405ada9df0 -> 0x307e34d9421e63d2ca92fab68ae720304927d6e8
PaymentManager proxy 0xef4364fe4487353df46eb7c811d4fac78b856c7f -> 0x56c3af6c5995147f293dc756216920fd24d50684
JobManager proxy 0x9c690c267f20c385f8a053f62bc8c7e2d4b83744 -> 0x96268da235ad5d41870fe753273d5ecf5394461d
PaymentManager stores per-job escrow accounting, but transfers are paid from the manager’s pooled USDC balance. Therefore strict per-job payout bounds are required to prevent cross-job fund bleed.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK ACT opportunity where escrow conservation and payout finality failed in combination. The required invariant is that cumulative released value for a job never exceeds job escrow. In historical PaymentManager code, releasePayment retained transfer logic but had hasSufficientEscrow check commented out, so over-release did not revert. In historical Router code, completed-job payout path was callable again through explicit claimBudget, with no one-time guard that prevents duplicate terminal payout. The attacker controlled creator/provider/evaluator roles on a synthetic job, so could deterministically drive phase progression to COMPLETED and then invoke an additional claim path. Because token transfers are from pooled vault liquidity, repeated release for the same job consumed third-party funds. This mechanism matches trace, logs, and balance deltas for the incident tx.
4. Detailed Root Cause Analysis
4.1 Invariant and code-level breakpoint
Expected safety invariant:
assert(escrowDetails[jobId].releasedAmount + amount <= escrowDetails[jobId].amount);
Historical PaymentManager_56c3.sol breakpoint:
function releasePayment(...) external override onlyACP nonReentrant {
require(recipient != address(0), "Zero address recipient");
require(amount > 0, "Zero amount");
// require(hasSufficientEscrow(jobId, amount), "Insufficient escrow");
...
escrowDetails[jobId].releasedAmount += amount;
IERC20(token).safeTransfer(platformTreasury, platformFee);
IERC20(token).safeTransfer(recipient, netAmount);
}
Historical ACPRouter_307e.sol duplicate payout path:
function claimBudget(uint256 jobId) external nonReentrant {
ACPTypes.Job memory job = jobManager.getJob(jobId);
...
_claimBudget(jobId);
}
function _claimBudget(uint256 jobId) internal {
ACPTypes.Job memory job = jobManager.getJob(jobId);
if (job.phase == ACPTypes.JobPhase.COMPLETED) {
paymentManager.releasePayment(job.id, job.provider, job.budget, job.evaluator, "Job completion payment");
}
}
4.2 On-chain mechanism and deterministic evidence
Decoded tx summary / trace shows:
createJob(... budget=97000000000)setEscrowDetails(jobId=1002573889, amount=97000000000, token=USDC)oncereleasePayment(jobId=1002573889, amount=97000000000, ...)twicePaymentReleased(... amount=77600000000 ...)twice
Representative decoded evidence:
paymentManager.setEscrowDetails(jobId=1002573889, amount=97000000000, token=USDC)
paymentManager.releasePayment(jobId=1002573889, amount=97000000000, ...) [called twice]
PaymentReleased(jobId=1002573889, recipient=0xc1ee..., amount=77600000000, token=USDC) [emitted twice]
Balance-diff evidence for tx:
- PaymentManager USDC delta:
-97000000000(-97,000 USDC) - Attacker EOA USDC delta:
+58200000000(+58,200 USDC) - Treasury USDC delta:
+38800000000(+38,800 USDC)
4.3 ACT predicate and fee accounting
Success predicate is attacker monetary gain in USDC:
- before
0, after58200000000, delta58200000000.
fees_paid_in_reference_asset is deterministically 0 because reference asset is USDC while execution fees are paid in native ETH. Native fee derivation from receipt:
gasUsed(0x94e39c = 9,757,596)
* effectiveGasPrice(0x6ded0a = 7,204,106 wei)
+ l1Fee(0xa1757d42f = 43,341,304,879 wei)
= 70,338,097,194,055 wei
This equals sender native delta in balance diff (-70338097194055 wei).
5. Adversary Flow Analysis
Adversary-related accounts in the incident:
- EOA:
0x79265e89feaf7e971dec75db1432795e6bd4b466(sender and profit receiver) - Attacker contract:
0xe02219e6978c96cc25570087393b4436fa0079f6 - Helper/provider contract:
0xc1ee502dd42bb5433b3fc7b153c9334255fc3c07
End-to-end on-chain sequence in one tx:
- Flash loan funding (
97,000 USDC) from Morpho. - Create attacker-controlled account/job and set budget.
- Drive phases
REQUEST -> NEGOTIATION -> TRANSACTION -> EVALUATION -> COMPLETEDvia attacker-controlled memos/signatures. - First payout through completion path (
_claimBudget -> releasePayment). - Second payout via explicit
claimBudget(jobId)(same budget released again). - Sweep payout, repay flash loan principal, keep residual profit.
This is ACT-feasible with public contracts and unprivileged addresses only.
6. Impact & Losses
Measured impact for tx 0xe94a5ed5...:
- Vault loss:
97,000 USDC(PaymentManager net depletion) - Attacker EOA gain:
58,200 USDC - Treasury gain from duplicated fee path:
38,800 USDC
Affected victim contracts:
- PaymentManager proxy
0xef4364fe4487353df46eb7c811d4fac78b856c7f - ACP Router proxy
0xa6c9ba866992cfd7fd6460ba912bfa405ada9df0
7. References
- Root cause artifact:
/workspace/session/root_cause.json - Exploit tx metadata/trace/balance diff:
/workspace/session/artifacts/collector/seed/8453/0xe94a5ed54d0a9aa317c997607d7d1ea9828ad47626d7794b0e4020ff49cdf9a0 - Decoded tx summary:
/workspace/session/artifacts/auditor/iter_0/evidence/tx_summary.txt - Call trace:
/workspace/session/artifacts/auditor/iter_0/evidence/calltrace.json - Receipt logs:
/workspace/session/artifacts/auditor/iter_0/evidence/receipt.json - Implementation slot snapshot:
/workspace/session/artifacts/auditor/iter_0/evidence/implementation_slots.txt - Historical Router source:
/workspace/session/artifacts/auditor/iter_0/evidence/ACPRouter_307e.sol - Historical PaymentManager source:
/workspace/session/artifacts/auditor/iter_0/evidence/PaymentManager_56c3.sol