jackygu's blog

发布于 2024-01-25到 Mirror 阅读

关于使用合约批量铸造的原因分析与解决方案

Cirth.meme上线4天,各方面表现良好。但铸造代码中,发现存在使用合约来批量铸造的可能,这严重影响了公平铸造。

类似的交易记录见:https://etherscan.io/tx/0xa58e8039740892f59b25ec0e14ce63d3d5c62eba0b16cdcd61c65553d5c67874。

原因

社区提交上述交易后,经技术团队研究,复现了其操作,大概的方式为:写两个智能合约,一个为主控制合约,一个为铸造合约,在主控制合约中创建多个铸造合约,通过铸造合约伪造正常用户,去调用Cirth合约的mint方法。

下面是实现类似功能的智能合约源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

interface IFERC721C {
	function mint(address to) external;
}

interface ICallMint {
	function mint() external;
	function destroy() external;
}

contract BatchMint is Ownable {
	address public immutable tokenImplementation;

	event Deploy(address);

	constructor(address _ferc721) {
		tokenImplementation = address(new CallMint(msg.sender, _ferc721));
	}

	function deployAndMint(uint times) public onlyOwner() {
		for(uint i; i < times; i++) {
			address tokenAddress = Clones.clone(tokenImplementation);
			ICallMint(tokenAddress).mint();
			ICallMint(tokenAddress).destroy();
			emit Deploy(tokenAddress);
		}
	}
}

contract CallMint {
	address public immutable ferc721;
	address public immutable receiver;

	constructor(address _receiver, address _ferc721) {
		ferc721 = _ferc721;
		receiver = _receiver;
	}

	function mint() public {
		IFERC721C(ferc721).mint(receiver);
	}

	function destroy() public {
		selfdestruct(payable(receiver));
	}
}

解释:

  • BatchMint:主控制合约 批量铸造合约,该合约在构建时,创造一个CallMint合约的模版,在批量铸造方法deployAndMint中,通过Clone方法生成多个调用合约(即B合约)

  • CallMint:铸造合约 单次铸造后,立即销毁自己,在链上不留痕迹。铸造时,由合约调用人接收NFT。

  • 该方法经测试,单个NFT铸造的Gas费比正常铸造高30%。

  • 如果在切换调用合约时,同时转Ferc,可以实现只要拥有10个Ferc,就能给N个铸造合约使用。

  • 该方法不需要手动在钱包中开多个账户,并且可以绕开冷冻时间。

解决方案

在铸造方法中增加一个对调用账户的限制,即仅允许EOA账户调用,拒绝合约账户调用(包括AA账户),如下:

function mint(address _to) public payable {
    require(_tokenData.totalSupply < _tokenData.max / _tokenData.limit, "touch the max batch");
    require(_to != address(0), "mint to zero address");
    require(msg.sender.code.length == 0 && _to.code.length == 0, "contract account not support"); // 增加该行
    ...
}

以上更新将在下一版中实施,但这个解决方案的缺陷在于,将无法支持AA账户。