We do not have a reliable USD price for the recorded assets yet.
0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f0xba169cceccf7ac51da223e04654cf16ef41a68ccArbitrum0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cdArbitrumOn Arbitrum block 147817766, transaction 0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f let an unprivileged adversary turn sound WBTC collateral inside a freshly minted The Standard vault into dust PAXG, leaving the vault with unbacked EUROs debt. The originating EOA 0x09ed480feaf4cbc363481717e04e2c394ab326b4 called helper contract 0xb589d4a36ef8766d44c9785131413a049d51dbc0, borrowed 10.00000010 WBTC from a public Uniswap V3 flash pool, minted a fresh vault, minted 290000 EUROs against 10 WBTC collateral, created a new WBTC/PAXG Uniswap V3 pool, and then forced the vault to swap its collateral through that attacker-created pool.
The root cause is a vault-side logic flaw in SmartVaultV2.swap(): the function routes owner-selected collateral swaps with amountOutMinimum = 0 and never checks whether the vault remains solvent after the swap. Because Uniswap V3 pool creation is permissionless, the attacker could create the exact WBTC/PAXG fee-3000 pool that the vault’s hardcoded route expected, seed it with dust liquidity at an arbitrary price, and convert 995000000 raw WBTC units from the vault into only 99999998982 raw PAXG units. The attacker then sold part of the freshly minted EUROs for USDC, bought back the WBTC needed for flash-loan repayment, and finished the transaction with EUROs and USDC while the protocol was left with a vault carrying EUROs of debt against negligible collateral.
2800008512.226242291450The Standard vault model allows a vault owner to deposit accepted collateral, mint EUROs against that collateral, and later manage the collateral through owner-only actions such as removeCollateral, removeAsset, and swap. The seed trace shows the relevant public protocol components in this incident:
0xba169cceccf7ac51da223e04654cf16ef41a68cc0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd0x643b34980e635719c15a2d4ce69571a258f940e90xE592427A0AEce92De3Edee1F18E0157C05861564The critical background detail is that Uniswap V3 pools are permissionless, while SmartVaultV2.swap() does not discover a trusted venue dynamically. It resolves token symbols through the token manager, hardcodes fee tier 3000, and sends the swap to the configured router. That means safety depends on the vault contract enforcing its own slippage and solvency invariants rather than trusting any pool that matches (tokenIn, tokenOut, fee).
The verified manager code also shows why a fresh vault is easy for any attacker to obtain. SmartVaultManagerV52::mint() is a public function that mints a vault NFT to the caller, deploys a new vault, and grants the vault mint/burn rights on EUROs:
function mint() external returns (address vault, uint256 tokenId) {
tokenId = lastToken + 1;
_safeMint(msg.sender, tokenId);
lastToken = tokenId;
vault = ISmartVaultDeployer(smartVaultDeployer).deploy(address(this), msg.sender, euros);
smartVaultIndex.addVaultAddress(tokenId, payable(vault));
IEUROs(euros).grantRole(IEUROs(euros).MINTER_ROLE(), vault);
IEUROs(euros).grantRole(IEUROs(euros).BURNER_ROLE(), vault);
}
Origin: verified SmartVaultManagerV52 source collected for the manager artifact.
This is an ATTACK-class ACT incident, not a privileged compromise. The broken invariant is straightforward: after any owner action, a vault’s debt should still satisfy minted <= maxMintable(). The vault enforces that invariant for explicit collateral removal, but not for collateral swaps.
The verified SmartVaultV2 source shows the asymmetry clearly. removeCollateral() and removeAsset() both gate withdrawals through canRemoveCollateral(), which prices the removal and ensures the remaining collateral still covers minted debt. mint() likewise enforces fullyCollateralised(). In contrast, swap() computes a fee, resolves the token addresses, builds an ExactInputSingleParams struct with amountOutMinimum: 0, and executes the swap without any post-condition that checks minted, maxMintable(), or undercollateralised().
That behavior is the code-level breakpoint. Once the attacker has minted EUROs against legitimate WBTC collateral, the vault owner can intentionally destroy collateral value by routing the swap through a pool with manipulated pricing and vanishing liquidity. Because the function accepts any accepted-token pair and uses the router’s normal pool lookup, an attacker can create the matching pool first and ensure the vault trades into the attacker’s venue.
The relevant source excerpt is below:
function removeCollateral(bytes32 _symbol, uint256 _amount, address _to) external onlyOwner {
ITokenManager.Token memory token = getTokenManager().getToken(_symbol);
require(canRemoveCollateral(token, _amount), UNDER_COLL);
IERC20(token.addr).safeTransfer(_to, _amount);
}
function removeAsset(address _tokenAddr, uint256 _amount, address _to) external onlyOwner {
ITokenManager.Token memory token = getTokenManager().getTokenIfExists(_tokenAddr);
if (token.addr == _tokenAddr) require(canRemoveCollateral(token, _amount), UNDER_COLL);
IERC20(_tokenAddr).safeTransfer(_to, _amount);
}
function swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount) external onlyOwner {
uint256 swapFee = _amount * ISmartVaultManagerV2(manager).swapFeeRate() / ISmartVaultManager(manager).HUNDRED_PC();
address inToken = getSwapAddressFor(_inToken);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: inToken,
tokenOut: getSwapAddressFor(_outToken),
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: _amount - swapFee,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
inToken == ISmartVaultManagerV2(manager).weth() ?
executeNativeSwapAndFee(params, swapFee) :
executeERC20SwapAndFee(params, swapFee);
}
Origin: verified SmartVaultV2 source from Sourcify for vault 0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd.
The security principles violated are:
amountOutMinimum = 0.The ACT opportunity is defined from the Arbitrum public pre-state immediately before the seed transaction in block 147817766. In that pre-state, the attacker did not need any privileged role, private key, or hidden off-chain artifact. The attacker only needed public protocol entrypoints plus temporary capital, which came from a public Uniswap V3 flash loan.
The seed trace shows the full exploit sequence:
0x09ed480feaf4cbc363481717e04e2c394ab326b4 called helper contract 0xb589d4a36ef8766d44c9785131413a049d51dbc0.1000000010 raw WBTC units from flash pool 0x2f5e87c9312fa29aed5c179e456625d79015299c.SmartVaultManagerV52::mint() and received vault 0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd.1000000000 raw WBTC units and minted 290000000000000000000000 raw EUROs units, while the protocol received a mint fee of 1450000000000000000000 raw EUROs units.3000 Uniswap V3 pool at 0x29046f8f9e7623a6a21cc8c3cc2a2121ae855b8d, initialized it at an attacker-chosen price corresponding to a 1e10 raw reserve ratio between PAXG wei and WBTC sats, and seeded it with only 10 raw WBTC units and 100000000000 raw PAXG units.vault.swap(WBTC, PAXG, 1000000000), causing the vault to send 5000000 raw WBTC units to the protocol as a swap fee and route the remaining 995000000 raw WBTC units through the attacker-created pool.99999998982 raw PAXG units in return, collapsing the economic value of the collateral while leaving the minted EUROs debt outstanding.10000000000000000000000 raw EUROs units for 10435742169 raw USDC units, bought 5500014 raw WBTC units to cover the flash-loan shortfall, repaid 1000500011 raw WBTC units to the flash pool, and transferred the remaining USDC and EUROs profit to the originating EOA.The most important trace fragment is the malicious swap itself:
0x2E9f9Cc46679DBb5D94a1397Bd922cA5F6dA99Cd::swap(WBTC, PAXG, 1000000000)
...
0xE592427A0AEce92De3Edee1F18E0157C05861564::exactInputSingle(
tokenIn=WBTC,
tokenOut=PAXG,
fee=3000,
recipient=0x2E9f9Cc46679DBb5D94a1397Bd922cA5F6dA99Cd,
amountIn=995000000,
amountOutMinimum=0
)
0x29046f8f9E7623a6A21Cc8c3CC2a2121aE855b8d::swap(...)
PAXG::transfer(vault, 99999998982)
WBTC::transferFrom(vault, pool, 995000000)
Origin: seed transaction trace.
This is the decisive failure mode. Before the swap, the collateral was real WBTC. After the swap, the vault still held an accepted token, but only in a quantity whose EUR value was dust relative to the outstanding debt. The analysis artifact reports the post-attack vault state as minted = 291450000000000000000000, maxMintable = 166525816665503, and totalCollateralValue = 183178398332054, which is far below one EURO in 1e18 scaling and therefore consistent with effective bad debt. The manager’s own liquidation path confirms the intended invariant boundary by requiring vault.undercollateralised() before liquidating a vault.
The exploit conditions in root_cause.json are supported by the same evidence:
mint() call.getAcceptedTokens().The adversary cluster consisted of:
0x09ed480feaf4cbc363481717e04e2c394ab326b4, which sent the seed transaction and received the final EUROs and USDC profit.0xb589d4a36ef8766d44c9785131413a049d51dbc0, which executed the flash loan, vault mint, pool creation, collateral swap, repayment, and final transfers.The attacker lifecycle has three concrete stages.
The helper borrowed 1000000010 raw WBTC units from the public pool 0x2f5e87c9312fa29aed5c179e456625d79015299c. The balance diff confirms the pool ended with +500001 raw WBTC units, which is the flash-loan fee.
The helper minted vault token ID 165, received vault 0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd, deposited 10 WBTC, and minted 290000 EUROs. It then created pool 0x29046f8f9e7623a6a21cc8c3cc2a2121ae855b8d with an attacker-chosen initialization price corresponding to a 1e10 raw reserve ratio and dust liquidity:
createAndInitializePoolIfNecessary(WBTC, PAXG, 3000, 7922816251426433759354395033600000)
mint(
token0=WBTC,
token1=PAXG,
fee=3000,
amount0Desired=10,
amount1Desired=100000000000
)
Origin: seed transaction trace.
After the vault swap, the helper monetized the bad debt:
10000 EUROs for 10435742169 raw USDC units in pool 0xc9aa2feb84f0134a38d5d1c56b1e787191327cb0.5500014 raw WBTC units for 1923515927 raw USDC units in pool 0xac70bd92f89e6739b3a08db9b6081a923912f73d.8512226242 raw USDC units and 280000000000000000000000 raw EUROs units to the originating EOA.The attack is ACT because every step uses permissionless public contracts and public data. The helper contract is not a source of privilege; it is only an attacker-deployed convenience wrapper that any unprivileged actor could replace with an equivalent contract.
The protocol impact is a bad-debt vault: the attacker extracted EUROs against real WBTC collateral and then intentionally destroyed the collateral value before repayment. The measurable protocol loss recorded in the analysis artifact is:
EUROs: 291450000000000000000000 raw units (291450 EUROs, 18 decimals)The attacker’s directly realized profit in the seed transaction was:
280000000000000000000000 raw EUROs units to the originating EOA8512226242 raw USDC units (8512.226242 USDC) to the originating EOA1496895400000000 wei of native gas spendThe vault’s collateral was not merely reduced; it was transformed from 10 WBTC into effectively worthless PAXG-sized dust under the protocol’s own collateral valuation, leaving the EUROs supply underbacked. This is why the correct loss accounting is the full bad debt, not just the attacker’s immediately monetized USDC.
0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f/workspace/session/artifacts/collector/seed/42161/0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f/metadata.json/workspace/session/artifacts/collector/seed/42161/0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f/trace.cast.log/workspace/session/artifacts/collector/seed/42161/0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f/balance_diff.json/workspace/session/artifacts/collector/seed/42161/0x7c1acd1a7ba8c5f9f511bc0274b71a12c4be543d/src/SmartVaultManagerV52.solhttps://repo.sourcify.dev/contracts/full_match/42161/0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd/sources/contracts/SmartVaultV2.sol/workspace/session/artifacts/validator/root_cause_challenge_result.json