“我”免费了 — APE 空投被薅分析

区块链安全 2年前 (2022) admin
818 0 0

前言

2022 年 03 月 18 日,著名的 NFT 项目 BAYC 开启空投,持有 BAYC NFT 的用户可凭手中的 NFT 领取对应的 APE 代币。但是空投刚开始不久,就被爆出存在被薅的消息,由于对 NFT 领域的攻击没有那么熟悉,所以特此进行一波详细的技术分析。

技术细节分析

本次的攻击交易为(https://etherscan.io/tx/0xeb8c3bebed11e2e4fcd30cbfc2fb3c55c4ca166003c7f7d319e78eaab9747098),由于这次问题是发生在以太坊上,我们把交易丢进 ethtx.info 中进行一个直观的分析。

“我”免费了 — APE 空投被薅分析

在分析展开后,首先映入眼帘的就是一笔闪电贷,而且是BAYC的闪电贷,那闪电贷做什么呢?做价格操控?为了弄清楚原因,需要对交易进行更深一步的分析,可以看到,接下来的操作中,有一笔 redeem 操作,展开这笔操作,竟然还发现一个奇怪的行为,就是突然就出现了 BAYC NFT 的转账。

“我”免费了 — APE 空投被薅分析

那这个 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 之后,就去空投合约领钱了。

“我”免费了 — APE 空投被薅分析

从这个行为来看,不难分析出空投合约的空投逻辑是有别于正常的快照空投逻辑,而是直接判断你当前是否有这个 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 空投被薅分析

版权声明:admin 发表于 2022年3月18日 下午12:33。
转载请注明:“我”免费了 — APE 空投被薅分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...