Calculated from recorded token losses using historical USD prices at the incident time.
0xf52a681bc76df1e3a61d9266e3a66c7388ef579d62373feb4fd0991d360068550x34b666992fcce34669940ab6b017fe11e5750799Arbitrum0x5e93c07a22111b327ee0eaec64028064448ae848Arbitrum0x5ed32847e33844155c18944ae84459404e432620ArbitrumOn Arbitrum, an unprivileged attacker used a helper contract and a 4500 WETH flash loan to manipulate the Camelot WETH/USDC spot price and then redeem fixed-rate CIT into WETH through CitadelRedeem at an inflated rate. In exploit transaction 0xf52a681bc76df1e3a61d9266e3a66c7388ef579d62373feb4fd0991d36006855, CitadelTreasury transferred 24.842391304488882786 WETH to the attacker helper, which repaid the flash loan and forwarded 21.326482002211388258 WETH to the attacker EOA 0xfcf88e5e1314ca3b6be7eed851568834233f8b49.
The root cause is that CitadelRedeem used Camelot’s live USDC -> WETH spot quote as the payout oracle for fixed-rate redemptions. The fixed-rate staking subsystem correctly computes a deterministic USD-denominated redemption amount, but the final WETH amount is derived from camelotRouter.getAmountsOut inside the same transaction. Because that quote depends directly on current pool reserves, an attacker can temporarily distort the pair, redeem while the manipulated price is live, and force CitadelTreasury to overpay WETH.
Citadel’s staking and redemption flow is split across three public contracts on Arbitrum:
CitadelStaking at 0x5e93c07a22111b327ee0eaec64028064448ae848CitadelRedeem at 0x34b666992fcce34669940ab6b017fe11e5750799CitadelTreasury at 0x5ed32847e33844155c18944ae84459404e432620CitadelStaking records every fixed-rate staking position with a fixedRateAtStaking snapshot and later computes the USD-denominated value redeemable by the staker. The relevant code is the fixed-rate aggregation loop in the verified source:
function getCITInUSDAllFixedRates(address user, uint256 amount) external view returns (uint256) {
uint256 amountToConvert = amount;
uint256 USDEquivalent = 0;
for (uint256 i = 0; i < stakings[user].length; i++) {
Staking memory staking = stakings[user][i];
if (staking.rate == 1 && amountToConvert > 0) {
uint256 _amountToConvert = amountToConvert > staking.amount ? staking.amount : amountToConvert;
amountToConvert -= _amountToConvert;
USDEquivalent += (_amountToConvert * staking.fixedRateAtStaking) / 1e18;
}
}
return USDEquivalent;
}
When a position is created, the same contract persists the fixed-rate snapshot into the staking record:
stakings[user].push(
Staking(token, amountAfterFee, 0, fixedRate, currentEpoch, rate, false)
);
CitadelTreasury is a passive payout vault. It does not price redemptions independently; it only verifies that CitadelRedeem is the caller and that enough token balance exists before transferring the requested amount:
function distributeRedeem(address token, uint256 amount, address user) public {
require(msg.sender == CITRedeem, "Only CITRedeem can call this function");
require(IERC20(token).balanceOf(address(this)) >= amount, "Not enough tokens in treasury");
IERC20(token).transfer(user, amount);
}
This means the economic correctness of the payout is entirely delegated to CitadelRedeem.
The bug is an oracle manipulation vulnerability in the fixed-rate WETH redemption path. CitadelStaking produces a deterministic USD-value for the redeemable fixed-rate CIT, so the protocol already has an internal notion of what the position is worth. However, CitadelRedeem does not settle that value in USDC or through a manipulation-resistant conversion source. Instead, when the redeemer requests WETH, it queries Camelot’s live USDC -> WETH quote and immediately uses that return value as the treasury payout amount. That design lets the caller choose a payout asset whose amount is derived from attacker-controlled transient AMM state. The vulnerability is therefore not in the staking math or treasury transfer logic in isolation; it is the composition of deterministic fixed-rate value with a same-transaction manipulable spot conversion. The concrete breakpoint is the fixed-rate WETH branch in CitadelRedeem.redeem, where the protocol converts the USD notional into WETH using getAmountsOut and then pays that amount from treasury.
The critical code path is in CitadelRedeem.redeem:
uint256 _amount = CITStaking.getCITInUSDAllFixedRates(msg.sender, amount);
...
if (underlying == 1) {
address[] memory path = new address[](2);
path[0] = address(USDC);
path[1] = address(WETH);
uint[] memory a = camelotRouter.getAmountsOut(_amount / 1e12, path);
amountInUnderlying = a[1];
}
...
treasury.distributeRedeem(tokenAddy, amountInUnderlying, msg.sender);
This branch first calls CitadelStaking.getCITInUSDAllFixedRates, which returns a fixed USD-denominated amount based on the stake snapshot, and then converts that USD amount into WETH by reading the Camelot router’s current reserves-derived quote. There is no TWAP, oracle delay, slippage guard, or treasury-side sanity check between the external quote and the final WETH transfer.
The exploit transaction trace shows the attacker realized exactly that weakness. The helper contract 0xfcbf411237ac830dc892edec054f15ba7f9ea5a6, deployed by attacker EOA 0xfcf88e5e1314ca3b6be7eed851568834233f8b49 in transaction 0x2b2f9771d5a0c9e3f7d0b77bed95cc5279d05b4928d49ad4d73ad7ff03865be0, first prepared a fixed-rate position by depositing 2653 CIT in transaction 0xcf75802229d440e4fbabb4d357fa1886c25e9a6b5c693e9e9573c71c15e2b0d3. The exploit then happened in a single transaction:
FlashPool::flash(..., 4500000000000000000000, ...)
CamelotRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
4500000000000000000000,
0,
[WETH, USDC],
helper,
address(0),
...
)
CitadelRedeem::redeem(underlying=1, token=0, amount=30951666666666666666, rate=1)
The trace and balance diff show the sequence and outcomes:
0xc31e54c7a869b9fcbecc14363cf510d1c41fa443 lent exactly 4500 WETH and ended with a 2.25 WETH gain, proving full repayment plus fee.0x84652bb2539513baf36e225c930fdd8eaa63ce27 received the attacker’s 4500 WETH swap, temporarily distorting the pool state used by getAmountsOut.CitadelStaking reduced the helper’s fixed-rate redeemable CIT position by 30.951666666666666666 CIT.CitadelTreasury lost 24.842391304488882786 WETH in the same transaction.21.326482002211388258 WETH and only paid 68317300000000 wei in gas.The core economic mismatch is straightforward. The fixed-rate redemption basis for the redeemed CIT was 123.806666666666666664 USD. Under the manipulated Camelot reserves, CitadelRedeem converted that notional into 24.842391304488882786 WETH. The same notional at the pre-manipulation reserves would have been worth roughly 0.054579282748590137 WETH. The protocol therefore paid treasury WETH according to a transient attacker-created price rather than a manipulation-resistant valuation.
The invariant violation can be stated precisely: redeeming fixed-rate CIT for WETH must not let the caller increase treasury payout merely by moving an external AMM spot price inside the redemption transaction. The breakpoint is where CitadelRedeem equates a live Camelot quote with the correct WETH obligation and immediately instructs treasury to transfer that amount.
The adversary flow has four concrete stages.
First, the attacker deployed helper contract 0xfcbf411237ac830dc892edec054f15ba7f9ea5a6 from EOA 0xfcf88e5e1314ca3b6be7eed851568834233f8b49 in block 174643945. The deployment artifact attributes the helper to the same EOA that later sent the exploit transaction.
Second, the helper created the prerequisite fixed-rate position in transaction 0xcf75802229d440e4fbabb4d357fa1886c25e9a6b5c693e9e9573c71c15e2b0d3 at block 174659184. That transaction deposited 2653 CIT into fixed-rate staking, creating the redeemable position later consumed by the exploit.
Third, in exploit transaction 0xf52a681bc76df1e3a61d9266e3a66c7388ef579d62373feb4fd0991d36006855 at block 174662727, the helper:
4500 WETH from the flash pool.CitadelRedeem.24.842391304488882786 WETH from CitadelTreasury.2.25 WETH fee, and forwarded profit to the attacker EOA.The transaction-level evidence from the balance diff is:
{
"treasury_weth_delta": "-24842391304488882786",
"attacker_eoa_weth_delta": "21326482002211388258",
"flash_pool_weth_delta": "2250000000000000000",
"staking_cit_delta": "-30951666666666666666"
}
Fourth, the helper later unwound the residual position in transaction 0x09105b771ada0c66f48786260929c0967fc822e037904ced6eac61284b6992d9, withdrawing remaining CIT back to the attacker cluster. That later cleanup is not required for exploit success, but it confirms the same helper controlled the lifecycle of the attack position.
The direct victim was CitadelTreasury, which lost 24.842391304488882786 WETH in the exploit transaction. The attacker EOA realized 21.326482002211388258 WETH of end-of-transaction profit, while the difference covered flash-loan fee and unwind costs inside the same transaction.
The affected public protocol components were:
CitadelRedeem at 0x34b666992fcce34669940ab6b017fe11e5750799CitadelStaking at 0x5e93c07a22111b327ee0eaec64028064448ae848CitadelTreasury at 0x5ed32847e33844155c18944ae84459404e432620The loss asset was:
24842391304488882786 raw units (24.842391304488882786 WETH, decimal=18)This was an ACT exploit. No privileged keys, governance permissions, or private attacker-only infrastructure were needed. The attacker used a self-deployed helper contract, public flash liquidity, public AMM liquidity, and permissionless Citadel entrypoints.
0xf52a681bc76df1e3a61d9266e3a66c7388ef579d62373feb4fd0991d36006855 on Arbitrum, including full trace and balance diff.0xcf75802229d440e4fbabb4d357fa1886c25e9a6b5c693e9e9573c71c15e2b0d3.0x2b2f9771d5a0c9e3f7d0b77bed95cc5279d05b4928d49ad4d73ad7ff03865be0.0x09105b771ada0c66f48786260929c0967fc822e037904ced6eac61284b6992d9.CitadelRedeem source, especially the fixed-rate WETH branch around lines 117-145.CitadelStaking source, especially getCITInUSDAllFixedRates and the fixed-rate staking snapshot logic.CitadelTreasury source, especially distributeRedeem.