Solidity合约测试中带签名校验的Mint签名生成方案
问题描述
为基于Ethier工具库开发的ERC721合约编写测试用例时,无法生成符合合约签名校验规则的有效签名,保留签名校验逻辑时测试无法正常执行。
合约实现代码
// SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.9 <0.9.0; import "@divergencetech/ethier/contracts/crypto/SignatureChecker.sol"; import "@divergencetech/ethier/contracts/crypto/SignerManager.sol"; import "@divergencetech/ethier/contracts/erc721/BaseTokenURI.sol"; import "@divergencetech/ethier/contracts/erc721/ERC721ACommon.sol"; import "@divergencetech/ethier/contracts/erc721/ERC721Redeemer.sol"; import "@divergencetech/ethier/contracts/sales/FixedPriceSeller.sol"; import "@divergencetech/ethier/contracts/utils/Monotonic.sol"; import "@openzeppelin/contracts/token/common/ERC2981.sol"; import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; interface ITokenURIGenerator { function tokenURI(uint256 tokenId) external view returns (string memory); } // @author divergence.xyz contract TestBirds is ERC721ACommon, BaseTokenURI, FixedPriceSeller, ERC2981, AccessControlEnumerable { using EnumerableSet for EnumerableSet.AddressSet; using ERC721Redeemer for ERC721Redeemer.Claims; using Monotonic for Monotonic.Increaser; bytes32 public constant EXPULSION_ROLE = keccak256("EXPULSION_ROLE"); constructor( string memory name, string memory symbol, string memory baseTokenURI, address payable beneficiary, address payable royaltyReceiver ) ERC721ACommon(name, symbol) BaseTokenURI(baseTokenURI) FixedPriceSeller( 2.5 ether, Seller.SellerConfig({ totalInventory: 10_000, lockTotalInventory: true, maxPerAddress: 0, maxPerTx: 0, freeQuota: 125, lockFreeQuota: false, reserveFreeQuota: true }), beneficiary ) { _setDefaultRoyalty(royaltyReceiver, 1000); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } function _handlePurchase( address to, uint256 n, bool ) internal override { _safeMint(to, n); } mapping(bytes32 => bool) public usedMessages; function mintPublic( address to, bytes32 nonce, bytes calldata sig ) external payable { signers.requireValidSignature( signaturePayload(to, nonce), sig, usedMessages ); _purchase(to, 1); } function alreadyMinted(address to, bytes32 nonce) external view returns (bool) { return usedMessages[ SignatureChecker.generateMessage(signaturePayload(to, nonce)) ]; } function signaturePayload(address to, bytes32 nonce) internal pure returns (bytes memory) { return abi.encodePacked(to, nonce); } function _baseURI() internal view override(BaseTokenURI, ERC721A) returns (string memory) { return BaseTokenURI._baseURI(); } ITokenURIGenerator public renderingContract; function setRenderingContract(ITokenURIGenerator _contract) external onlyOwner { renderingContract = _contract; } function tokenURI(uint256 tokenId) public view override returns (string memory) { if (address(renderingContract) != address(0)) { return renderingContract.tokenURI(tokenId); } return super.tokenURI(tokenId); } function setRoyaltyInfo(address receiver, uint96 feeBasisPoints) external onlyOwner { _setDefaultRoyalty(receiver, feeBasisPoints); } function supportsInterface(bytes4 interfaceId) public view override(ERC721ACommon, ERC2981, AccessControlEnumerable) returns (bool) { return super.supportsInterface(interfaceId); } }
原有测试代码
const { expect } = require('chai'); describe("TestBirds", function () { it ("Should return correct name, URI, owner and beneficiary", async function () { const [owner, addr1] = await hre.ethers.getSigners() provider = ethers.provider const TestBirdsContract = await hre.ethers.getContractFactory("TestBirds") const testBirdsContractDeployed = await TestBirdsContract.deploy( "TestBirds", "APFP", "https://test.url/", owner.address, owner.address) console.log(await provider.getBalance(owner.address)); await testBirdsContractDeployed.deployed() const nonce = await ethers.provider.getTransactionCount(owner.address, "latest") await testBirdsContractDeployed.mintPublic(owner.address, nonce, signature???) expect(await testBirdsContractDeployed.name()).to.equal("TestBirds") expect(await testBirdsContractDeployed.tokenURI(0), "https://test.url/0") expect(await testBirdsContractDeployed.ownerOf(0)).to.equal(owner.address) }) })
解决方案
原有代码存在3个核心问题导致签名校验不通过:
- 合约继承的
SignerManager初始签名者列表为空,部署后没有将签名使用的地址加入白名单,就算签名格式正确也会被判定为无效 - 签名payload拼接、签名逻辑没有对齐合约内的校验规则:Ethier的
SignatureChecker采用标准ERC191个人签名方案,和ethers.js的signMessage逻辑完全兼容,但需要严格按照合约内abi.encodePacked(to, nonce)的规则拼接原始待签名数据 - 调用
mintPublic时没有携带足够的ETH,公售单价为2.5ETH,未传value参数时_purchase逻辑会直接报错;另外用链上交易计数当nonce容易出现重复,测试中直接生成随机bytes32即可。
修复后可运行的测试代码
const { expect } = require('chai'); const { ethers } = require("hardhat"); describe("TestBirds", function () { it ("Should mint successfully with valid signature", async function () { const [owner, minter] = await ethers.getSigners(); const MINT_PRICE = ethers.utils.parseEther("2.5"); const TestBirds = await ethers.getContractFactory("TestBirds"); const contract = await TestBirds.deploy( "TestBirds", "APFP", "https://test.url/", owner.address, owner.address ); await contract.deployed(); // 将签名者地址(测试用owner即可)加入合约签名白名单 await contract.addSigner(owner.address); // 生成随机nonce避免重复 const nonce = ethers.utils.randomBytes(32); const minterAddress = minter.address; // 严格对齐合约abi.encodePacked(to, nonce)规则拼接待签名数据 const payload = ethers.utils.solidityPack( ["address", "bytes32"], [minterAddress, nonce] ); // 用签名者钱包生成符合ERC191标准的个人签名 const signature = await owner.signMessage(ethers.utils.arrayify(payload)); // 调用铸造方法,传入正确参数并附带足够的铸造费用 await contract.connect(minter).mintPublic( minterAddress, nonce, signature, { value: MINT_PRICE } ); // 校验铸造结果 expect(await contract.name()).to.equal("TestBirds"); expect(await contract.tokenURI(0)).to.equal("https://test.url/0"); expect(await contract.ownerOf(0)).to.equal(minterAddress); expect(await contract.alreadyMinted(minterAddress, nonce)).to.equal(true); }) })
测试中如果需要用其他地址当签名者,只需要把对应地址加入签名白名单,用对应地址的signer执行签名即可,逻辑完全一致。生产环境下签名私钥不要和部署管理员私钥混用,避免私钥泄露导致盗铸。
内容的提问来源于stack exchange,提问作者Matias Villanueva
相关产品推荐
相关产品推荐

