前言
2022 年 03 月 18 日,著名的 NFT 项目 BAYC 开启空投,持有 BAYC NFT 的用户可凭手中的 NFT 领取对应的 APE 代币。但是空投刚开始不久,就被爆出存在被薅的消息,由于对 NFT 领域的攻击没有那么熟悉,所以特此进行一波详细的技术分析。
技术细节分析
本次的攻击交易为(https://etherscan.io/tx/0xeb8c3bebed11e2e4fcd30cbfc2fb3c55c4ca166003c7f7d319e78eaab9747098),由于这次问题是发生在以太坊上,我们把交易丢进 ethtx.info
中进行一个直观的分析。
在分析展开后,首先映入眼帘的就是一笔闪电贷,而且是BAYC的闪电贷,那闪电贷做什么呢?做价格操控?为了弄清楚原因,需要对交易进行更深一步的分析,可以看到,在接下来的操作中,有一笔 redeem
操作,展开这笔操作,竟然还发现一个奇怪的行为,就是突然就出现了 BAYC NFT
的转账。
那这个 redeem
操作究竟是怎么赎回 BAYC NFT
的呢?我们需要对对应的代码进行分析:
function redeem(uint256 amount, uint256[] calldata specificIds)
external
override
virtual
returns (uint256[] memory)
{
return redeemTo(amount, specificIds, msg.sender);
}
function redeemTo(uint256 amount, uint256[] memory specificIds, address to)
public
override
virtual
nonReentrant
returns (uint256[] memory)
{
onlyOwnerIfPaused(2);
require(
amount == specificIds.length || enableRandomRedeem,
"NFTXVault: Random redeem not enabled"
);
require(
specificIds.length == 0 || enableTargetRedeem,
"NFTXVault: Target redeem not enabled"
);
// We burn all from sender and mint to fee receiver to reduce costs.
_burn(msg.sender, base * amount);
// Pay the tokens + toll.
(, uint256 _randomRedeemFee, uint256 _targetRedeemFee, ,) = vaultFees();
uint256 totalFee = (_targetRedeemFee * specificIds.length) + (
_randomRedeemFee * (amount - specificIds.length)
);
_chargeAndDistributeFees(msg.sender, totalFee);
// Withdraw from vault.
uint256[] memory redeemedIds = withdrawNFTsTo(amount, specificIds, to);
emit Redeemed(redeemedIds, specificIds, to);
return redeemedIds;
}
以上是 redeem
的代码,从逻辑上来看并不复杂,只是调用了 redeemTo
函数,并再对应的 L#28 行燃烧掉用户的某种代币,然后就把合约中的 BAYC NFT
给到了用户。但是分析到这里,我不禁有个疑问,就是这个合约做咩会有这个珍贵的 BAYC NFT
呢?为了稍微的了解下具体的业务逻辑,我翻了下合约,发现有一个叫 mintTo
的函数给出了答案
function mintTo(
uint256[] memory tokenIds,
uint256[] memory amounts, /* ignored for ERC721 vaults */
address to
) public override virtual nonReentrant returns (uint256) {
onlyOwnerIfPaused(1);
require(enableMint, "Minting not enabled");
// Take the NFTs.
uint256 count = receiveNFTs(tokenIds, amounts);
// Mint to the user.
_mint(to, base * count);
uint256 totalFee = mintFee() * count;
_chargeAndDistributeFees(to, totalFee);
emit Minted(tokenIds, amounts, to);
return count;
}
这个函数的大概意思呢,就是你的 BAYC NFT
给到合约,合约会根据你转入的 BAYC NFT
的数量,给你铸对应数量的他自己的代币,所以合约的 BAYC NFT
是从这里来的。
回到我们的分析上来,通过结合 mintTo
函数和 redeem
函数,我们已经知道了这个流程的大概逻辑,其实就是你可以拿着你的 BAYC NFT
到这个合约里,合约就会给你这个 BAYC
代币,也就是攻击者闪电贷的这个代币,同时呢,你的 BAYC NFT
就给合约了,反过来,如果你拿着 BAYC
这个代币到合约里,通过 redeem
函数,你也是可以赎回放在合约里的 BAYC NFT
。大概逻辑是这样。同时,细心的同学也可以发现,攻击者的闪电贷也是从这个合约发起的。也就是说,这个合约提供了 铸币
、赎回
和 闪电贷
功能。
那么继续往下看, 攻击者一拿到对应的 BAYC NFT
之后,就去空投合约领钱了。
从这个行为来看,不难分析出空投合约的空投逻辑是有别于正常的快照空投逻辑,而是直接判断你当前是否有这个 BAYC NFT
,然后根据当前持有数量来决定你的空投数量。为了验证这个想法,我们需要对空投逻辑进行确认
function claimTokens() external whenNotPaused {
require(block.timestamp >= claimStartTime && block.timestamp < claimStartTime + claimDuration, "Claimable period is finished");
require((beta.balanceOf(msg.sender) > 0 || alpha.balanceOf(msg.sender) > 0), "Nothing to claim");
uint256 tokensToClaim;
uint256 gammaToBeClaim;
(tokensToClaim, gammaToBeClaim) = getClaimableTokenAmountAndGammaToClaim(msg.sender);
for(uint256 i; i < alpha.balanceOf(msg.sender); ++i) {
uint256 tokenId = alpha.tokenOfOwnerByIndex(msg.sender, i);
if(!alphaClaimed[tokenId]) {
alphaClaimed[tokenId] = true;
emit AlphaClaimed(tokenId, msg.sender, block.timestamp);
}
}
for(uint256 i; i < beta.balanceOf(msg.sender); ++i) {
uint256 tokenId = beta.tokenOfOwnerByIndex(msg.sender, i);
if(!betaClaimed[tokenId]) {
betaClaimed[tokenId] = true;
emit BetaClaimed(tokenId, msg.sender, block.timestamp);
}
}
uint256 currentGammaClaimed;
for(uint256 i; i < gamma.balanceOf(msg.sender); ++i) {
uint256 tokenId = gamma.tokenOfOwnerByIndex(msg.sender, i);
if(!gammaClaimed[tokenId] && currentGammaClaimed < gammaToBeClaim) {
gammaClaimed[tokenId] = true;
emit GammaClaimed(tokenId, msg.sender, block.timestamp);
currentGammaClaimed++;
}
}
grapesToken.safeTransfer(msg.sender, tokensToClaim);
totalClaimed += tokensToClaim;
emit AirDrop(msg.sender, tokensToClaim, block.timestamp);
}
这个空投逻辑就简单了,直接就是在 L#10-16, L#26-34 判断你当前是否拥有对应的 BAYC NFT
,然后就根据你的持有的 BACY NFT
数量来派发 APE
代币,同时已经用来领过代币的 BAYC NFT
就不能再领了。
所以,这次整个薅羊毛的逻辑其实就是,从官方的合约里闪电贷出来 BAYC
代币,然后用这个代币赎回合约里的 NFT
然后使用这个 NFT
去领取空投,因为空投合约是判断领取时是否持有的,而不是在某个时间段是否持有,所以攻击者可以直接绕过需要持有 NFT
这个条件,免费捡大饼。
在后续网上有说可以使用持有时长来判定领取条件的,但是我想了下, BAYC NFT
本身是没有类似 OpenZeppelin
中 ERC20Snapshot
类似的快照功能的,所以如果要使用的话,只能是用直接链下快照空投这种模式,会好很多。
原文始发于微信公众号(蛋蛋的区块链笔记):“我”免费了 — APE 空投被薅分析