Calculated from recorded token losses using historical USD prices at the incident time.
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d60x598c6c1cd9459f882530fc9d7da438cb74c6cb3bArbitrumOn Arbitrum block 202973713, transaction 0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6 drained the Rico BankDiamond wallet 0x598c6c1cd9459f882530fc9d7da438cb74c6cb3b. The attacker EOA 0xc91cb089084f0126458a1938b794aa73b9f9189d used helper contract 0x68d843d31de072390d41bff30b0076bef0482d8f to make the wallet execute arbitrary ERC20 transfers as itself, then converted most of the stolen assets into USDC and paid out 22456.465050 USDC to the attacker EOA.
The root cause was a permissionless arbitrary-call primitive in Rico's Vat facet. BankDiamond routed selector 0xa0a7930a to Vat facet 0xc6D7b37FE18A3Dd007F9b1C3b339B8c6043b3ccf, and Vat.flash(address,bytes) performed code.call(data) without any owner or caller authorization check. That broke the wallet's core custody invariant: an unprivileged address could force the wallet to call token contracts as the wallet and transfer away wallet-held balances.
Rico uses a diamond-style BankDiamond proxy. The wallet exposes a facetAddress(bytes4) lookup and dispatches unknown selectors through its fallback to the configured facet. At the exploit block, selector 0xa0a7930a resolved to the Vat facet, so external callers could reach Vat.flash(address,bytes) through the wallet.
The verified Vat source shows that flash is not owner-gated, unlike administrative entrypoints such as init, filk, and fold, which are explicitly marked onlyOwner. Instead, flash mints temporary RICO to the supplied code address, executes code.call(data), then burns the temporary RICO again.
Because the external call is executed from the BankDiamond context, msg.sender inside downstream token contracts is the wallet itself. Any ERC20 transfer or transferFrom calldata therefore moves assets out of the wallet if balances or allowances exist. The helper contract seen in the incident only batches these calls; the exploit primitive is the wallet's public flash entrypoint.
The vulnerability class is an unrestricted arbitrary external call exposed from a custody wallet. The victim proxy 0x598c6c1cd9459f882530fc9d7da438cb74c6cb3b routed selector 0xa0a7930a to Vat facet 0xc6D7b37FE18A3Dd007F9b1C3b339B8c6043b3ccf, making flash(address,bytes) reachable from any external caller. In the verified source, flash takes arbitrary code and data parameters and executes code.call(data) without checking that msg.sender is the wallet owner or another trusted actor. That design means any unprivileged address can instruct the wallet to call ERC20 contracts as the wallet itself. The safety invariant that only authorized execution paths may move wallet-held assets is therefore violated at the function entrypoint. The exploit does not depend on compromised keys, admin participation, or attacker-only contracts; those were convenience tools layered on top of an already permissionless arbitrary-call bug.
The code-level breakpoint is the flash implementation in the verified Vat facet:
function flash(address code, bytes calldata data)
external payable returns (bytes memory result) {
VatStorage storage vs = getVatStorage();
if (vs.flock == LOCKED) revert ErrLock();
vs.flock = LOCKED;
getBankStorage().rico.mint(code, _MINT);
bool ok;
(ok, result) = code.call(data);
if (!ok) bubble(result);
getBankStorage().rico.burn(code, _MINT);
vs.flock = UNLOCKED;
}
This function is externally callable and lacks onlyOwner or equivalent caller validation. In the verified BankDiamond metadata, the wallet exposes facetAddress(bytes4) and owner(), which is enough to confirm the intended ownership model and the diamond routing behavior.
The exploit trace shows the exact unauthorized path. The helper contract calls the victim wallet, which dispatches into Vat::flash, and the wallet then transfers its own USDC to the helper:
0x68d843...::a558c55d(...)
BankDiamond::fallback(FiatTokenProxy, 0xa9059cbb...)
Vat::flash(FiatTokenProxy, 0xa9059cbb...) [delegatecall]
FiatTokenV2_2::transfer(0x68d843..., 10375584869)
emit Transfer(from: BankDiamond, to: 0x68d843..., value: 10375584869)
The same trace pattern repeats for ARB, LINK, wstETH, WETH, and USDC.e. For example, the ARB leg shows BankDiamond::fallback(...) -> Vat::flash(...) -> transfer(0x68d843..., 2478235540754361715555), and the storage diff zeroes the victim's tracked balance slot. The balance-diff artifact independently confirms that all six wallet balances went from their pre-state amounts to zero:
{
"USDC": "10375584869 -> 0",
"ARB": "2478235540754361715555 -> 0",
"LINK": "69669705859926416088 -> 0",
"wstETH":"266618989782460718 -> 0",
"WETH": "287173939050505854 -> 0",
"USDC.e":"300000000 -> 0"
}
The exploit then continued beyond direct draining. The helper contract used the drained assets and pre-existing allowances to run swap and transferFrom legs, ending with a direct USDC payout to the attacker EOA:
FiatTokenV2_2::balanceOf(0x68d843...) -> 22456465050
FiatTokenV2_2::transfer(0xc91cb089084f0126458a1938b794aa73b9f9189d, 22456465050)
emit Transfer(from: 0x68d843..., to: 0xc91cb089..., value: 22456465050)
That post-drain monetization is not the root cause. The exploit was already complete once an arbitrary caller could make the wallet execute ERC20 transfers as itself.
The attacker flow was a single transaction initiated by EOA 0xc91cb089084f0126458a1938b794aa73b9f9189d. The transaction targeted helper contract 0x68d843d31de072390d41bff30b0076bef0482d8f, which accepted the victim wallet address and token list as parameters. The helper then queried the victim's balances and repeatedly called the victim wallet with calldata that resolved to Vat.flash(token, abi.encodeWithSelector(IERC20.transfer.selector, helper, amount)).
Each flash call executed from the wallet context, so each token contract observed the wallet as the caller and moved the wallet-held balance to the helper. The trace demonstrates this for all six drained assets. After direct draining, the helper used on-chain venues, including Uniswap V3-style swap paths, to convert most assets into USDC. The final leg transferred 22456465050 raw USDC units from the helper to the attacker EOA.
The important decision point is that the helper contract was optional. Any unprivileged actor who knew the wallet address and token balances could have directly called victim.flash(token, transferCalldata) from a fresh address. That is why the incident is correctly classified as an ACT opportunity.
The victim BankDiamond wallet was fully depleted of the six tracked ERC20 balances present immediately before the exploit transaction:
10375584869 raw units2478235540754361715555 raw units69669705859926416088 raw units266618989782460718 raw units287173939050505854 raw units300000000 raw unitsThe attacker monetized the theft inside the same transaction and ended with 22456465050 raw USDC units in EOA 0xc91cb089084f0126458a1938b794aa73b9f9189d. The impact is a direct custody failure of the Rico wallet architecture: an unprivileged external caller could empty wallet-held assets without owner consent.
0x5d2a94785d95a740ec5f778e79ff014c880bcefec70d1a7c2440e611f84713d6 on Arbitrum0x598c6c1cd9459f882530fc9d7da438cb74c6cb3b0xc6D7b37FE18A3Dd007F9b1C3b339B8c6043b3ccf0xc91cb089084f0126458a1938b794aa73b9f9189d0x68d843d31de072390d41bff30b0076bef0482d8fhttps://repo.sourcify.dev/contracts/full_match/42161/0x598c6c1cd9459f882530fc9d7da438cb74c6cb3b/metadata.jsonhttps://repo.sourcify.dev/contracts/full_match/42161/0xc6D7b37FE18A3Dd007F9b1C3b339B8c6043b3ccf/sources/src/vat.sol