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

By [jackygu's blog](https://paragraph.com/@jackygu) · 2024-01-25

---

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

类似的交易记录见：[https://etherscan.io/tx/0xa58e8039740892f59b25ec0e14ce63d3d5c62eba0b16cdcd61c65553d5c67874。](https://etherscan.io/tx/0xa58e8039740892f59b25ec0e14ce63d3d5c62eba0b16cdcd61c65553d5c67874%E3%80%82)

原因
--

社区提交上述交易后，经技术团队研究，复现了其操作，大概的方式为：写两个智能合约，一个为主控制合约，一个为铸造合约，在主控制合约中创建多个铸造合约，通过铸造合约伪造正常用户，去调用`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账户。

---

*Originally published on [jackygu's blog](https://paragraph.com/@jackygu/R8dA32cBiKlSSJ9BiOkk)*
