Root cause:
The vulnerability in the redeem() function of the Redemption smart contract allowed the attacker to manipulate the WETH/USDC.E pool price on Camelot. This caused the getAmountsOut() function to overestimate the value of deposited LP tokens, resulting in inflated redemption amounts.
Vulnerable code snippet:
function redeem(uint256 underlying, uint256 token, uint256 amount, uint8 rate) public nonReentrant {
require(underlying == 0 || underlying == 1, "Invalid underlying");
require(token == 0 || token == 1, "Invalid token");
require(rate == 0 || rate == 1, "Invalid rate");
require(amount > 0, "Amount must be greater than 0");
uint256 amountAvailable = CITStaking.redeemCalculator(msg.sender)[token][rate];
require(amountAvailable > 0, "Nothing to redeem");
uint256 amountInUnderlying;
address tokenAddy = underlying == 0 ? address(USDC) : address(WETH);
// Variable rate
if (rate == 0) {
require(amount <= amountAvailable, "Not enough CIT or bCIT to redeem");
require(amount <= maxRedeemableVariable, "Amount too high");
maxRedeemableVariable -= amount;
address[] memory path = new address[](3);
path[0] = address(CIT); // 1e18
path[1] = address(WETH);
path[2] = address(USDC); // 1e6
uint[] memory a = camelotRouter.getAmountsOut(amount, path);
if (underlying == 0) {
amountInUnderlying = a[2]; // result in 6 decimal
} else {
amountInUnderlying = a[1]; // result in 18 decimal
}
}
// Fixed rate
else {
uint256 _amount = CITStaking.getCITInUSDAllFixedRates(msg.sender, amount);
require(amount <= amountAvailable, "Not enough CIT or bCIT to redeem");
require(amount <= maxRedeemableFixed, "Amount too high");
maxRedeemableFixed -= amount;
if (underlying == 1) {
address[] memory path = new address[](2);
path[0] = address(USDC); // 1e6
path[1] = address(WETH); // 1e18
uint[] memory a = camelotRouter.getAmountsOut(_amount / 1e12, path); // result in 18 decimal
amountInUnderlying = a[1];
} else {
amountInUnderlying = _amount / 1e12; // 1e6 is the decimals of USDC, so 18 - 12 = 6
}
}
if (token == 0) {
CIT.burn(CITStakingAddy, amount);
CITStaking.removeStaking(msg.sender, address(CIT), rate, amount);
} else if (token == 1) {
totalbCITRedeemedByUser[msg.sender] += amount;
bCIT.burn(CITStakingAddy, amount);
CITStaking.removeStaking(msg.sender, address(bCIT), rate, amount);
}
treasury.distributeRedeem(tokenAddy, amountInUnderlying, msg.sender);
}
Attack transaction:
https://phalcon.blocksec.com/explorer/tx/arbitrum/0xf52a681bc76df1e3a61d9266e3a66c7388ef579d62373feb4fd0991d36006855
Analysis:
https://medium.com/neptune-mutual/how-was-citadel-finance-exploited-a5f9acd0b408