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