Calculated from recorded token losses using historical USD prices at the incident time.
0xe94a5ed54d0a9aa317c997607d7d1ea9828ad47626d7794b0e4020ff49cdf9a00xef4364fe4487353df46eb7c811d4fac78b856c7fBase0xa6c9ba866992cfd7fd6460ba912bfa405ada9df0BaseIn 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:
PaymentManager implementation 0x56c3af6c5995147f293dc756216920fd24d50684 had escrow sufficiency check disabled in releasePayment.ACPRouter implementation 0x307e34d9421e63d2ca92fab68ae720304927d6e8 allowed non-idempotent payout invocation through _claimBudget and explicit claimBudget(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.
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.
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.
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");
}
}
Decoded tx summary / trace shows:
createJob(... budget=97000000000)setEscrowDetails(jobId=1002573889, amount=97000000000, token=USDC) oncereleasePayment(jobId=1002573889, amount=97000000000, ...) twicePaymentReleased(... amount=77600000000 ...) twiceRepresentative 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:
-97000000000 (-97,000 USDC)+58200000000 (+58,200 USDC)+38800000000 (+38,800 USDC)Success predicate is attacker monetary gain in USDC:
0, after 58200000000, delta 58200000000.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).
Adversary-related accounts in the incident:
0x79265e89feaf7e971dec75db1432795e6bd4b466 (sender and profit receiver)0xe02219e6978c96cc25570087393b4436fa0079f60xc1ee502dd42bb5433b3fc7b153c9334255fc3c07End-to-end on-chain sequence in one tx:
97,000 USDC) from Morpho.REQUEST -> NEGOTIATION -> TRANSACTION -> EVALUATION -> COMPLETED via attacker-controlled memos/signatures._claimBudget -> releasePayment).claimBudget(jobId) (same budget released again).This is ACT-feasible with public contracts and unprivileged addresses only.
Measured impact for tx 0xe94a5ed5...:
97,000 USDC (PaymentManager net depletion)58,200 USDC38,800 USDCAffected victim contracts:
0xef4364fe4487353df46eb7c811d4fac78b856c7f0xa6c9ba866992cfd7fd6460ba912bfa405ada9df0/workspace/session/root_cause.json/workspace/session/artifacts/collector/seed/8453/0xe94a5ed54d0a9aa317c997607d7d1ea9828ad47626d7794b0e4020ff49cdf9a0/workspace/session/artifacts/auditor/iter_0/evidence/tx_summary.txt/workspace/session/artifacts/auditor/iter_0/evidence/calltrace.json/workspace/session/artifacts/auditor/iter_0/evidence/receipt.json/workspace/session/artifacts/auditor/iter_0/evidence/implementation_slots.txt/workspace/session/artifacts/auditor/iter_0/evidence/ACPRouter_307e.sol/workspace/session/artifacts/auditor/iter_0/evidence/PaymentManager_56c3.sol