The Standard Self-Swap Bad Debt
Exploit Transactions
0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9fVictim Addresses
0xba169cceccf7ac51da223e04654cf16ef41a68ccArbitrum0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cdArbitrumLoss Breakdown
Similar Incidents
Paribus Redeem Reentrancy
31%Rodeo Oracle Shortfall
31%Sentiment Balancer Oracle Overborrow
30%DEI burnFrom Allowance Inversion
30%Themis Oracle Manipulation
29%dForce Oracle Reentrancy Liquidation
29%Root Cause Analysis
The Standard Self-Swap Bad Debt
1. Incident Overview TL;DR
On 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 280000 EUROs and 8512.226242 USDC while the protocol was left with a vault carrying 291450 EUROs of debt against negligible collateral.
2. Key Background
The 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:
- SmartVaultManager at
0xba169cceccf7ac51da223e04654cf16ef41a68cc - Freshly deployed vault at
0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd - EUROs token at
0x643b34980e635719c15a2d4ce69571a258f940e9 - Uniswap V3 router at
0xE592427A0AEce92De3Edee1F18E0157C05861564
The 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.
3. Vulnerability Analysis & Root Cause Summary
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:
- Collateral-preserving operations must enforce the same solvency invariant as explicit collateral withdrawals.
- DEX swaps of collateral must enforce meaningful output bounds, not
amountOutMinimum = 0. - Permissionless AMM pools cannot be treated as trusted price-discovery sources without independent validation.
4. Detailed Root Cause Analysis
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:
- The EOA
0x09ed480feaf4cbc363481717e04e2c394ab326b4called helper contract0xb589d4a36ef8766d44c9785131413a049d51dbc0. - The helper borrowed
1000000010raw WBTC units from flash pool0x2f5e87c9312fa29aed5c179e456625d79015299c. - The helper called
SmartVaultManagerV52::mint()and received vault0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd. - The helper deposited
1000000000raw WBTC units and minted290000000000000000000000raw EUROs units, while the protocol received a mint fee of1450000000000000000000raw EUROs units. - The helper created a new WBTC/PAXG fee-
3000Uniswap V3 pool at0x29046f8f9e7623a6a21cc8c3cc2a2121ae855b8d, initialized it at an attacker-chosen price corresponding to a1e10raw reserve ratio between PAXG wei and WBTC sats, and seeded it with only10raw WBTC units and100000000000raw PAXG units. - The helper invoked
vault.swap(WBTC, PAXG, 1000000000), causing the vault to send5000000raw WBTC units to the protocol as a swap fee and route the remaining995000000raw WBTC units through the attacker-created pool. - The vault received only
99999998982raw PAXG units in return, collapsing the economic value of the collateral while leaving the minted EUROs debt outstanding. - The helper sold
10000000000000000000000raw EUROs units for10435742169raw USDC units, bought5500014raw WBTC units to cover the flash-loan shortfall, repaid1000500011raw 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:
- The attacker can mint or control a fresh vault NFT: shown by the public
mint()call. - The pair must be supported by the token manager: the trace shows both WBTC and PAXG returned from
getAcceptedTokens(). - A permissionless router path must exist for the fixed fee tier: the attacker created the exact Uniswap V3 pool the router used.
- Temporary capital is needed for the initial deposit and fee: satisfied by the public flash loan.
5. Adversary Flow Analysis
The adversary cluster consisted of:
- EOA
0x09ed480feaf4cbc363481717e04e2c394ab326b4, which sent the seed transaction and received the final EUROs and USDC profit. - Helper contract
0xb589d4a36ef8766d44c9785131413a049d51dbc0, which executed the flash loan, vault mint, pool creation, collateral swap, repayment, and final transfers.
The attacker lifecycle has three concrete stages.
Flash Funding
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.
Vault Minting and Malicious Pool Setup
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.
Collateral Destruction and Profit Realization
After the vault swap, the helper monetized the bad debt:
- Sold
10000EUROs for10435742169raw USDC units in pool0xc9aa2feb84f0134a38d5d1c56b1e787191327cb0. - Bought
5500014raw WBTC units for1923515927raw USDC units in pool0xac70bd92f89e6739b3a08db9b6081a923912f73d. - Repaid the flash loan plus fee.
- Sent
8512226242raw USDC units and280000000000000000000000raw 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.
6. Impact & Losses
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:291450000000000000000000raw units (291450EUROs,18decimals)
The attacker’s directly realized profit in the seed transaction was:
280000000000000000000000raw EUROs units to the originating EOA8512226242raw USDC units (8512.226242USDC) to the originating EOA- minus
1496895400000000wei of native gas spend
The 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.
7. References
- Seed transaction:
0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f - Seed transaction metadata:
/workspace/session/artifacts/collector/seed/42161/0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f/metadata.json - Seed transaction trace:
/workspace/session/artifacts/collector/seed/42161/0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f/trace.cast.log - Seed transaction balance diff:
/workspace/session/artifacts/collector/seed/42161/0x51293c1155a1d33d8fc9389721362044c3a67e0ac732b3a6ec7661d47b03df9f/balance_diff.json - Collected SmartVaultManager source:
/workspace/session/artifacts/collector/seed/42161/0x7c1acd1a7ba8c5f9f511bc0274b71a12c4be543d/src/SmartVaultManagerV52.sol - Verified SmartVaultV2 source (Sourcify):
https://repo.sourcify.dev/contracts/full_match/42161/0x2e9f9cc46679dbb5d94a1397bd922ca5f6da99cd/sources/contracts/SmartVaultV2.sol - Validator root-cause challenge result:
/workspace/session/artifacts/validator/root_cause_challenge_result.json