如何限制仅能通过自研dApp进行NFT mint,禁止直接调用智能合约铸造
ERC721 铸造权限限制(仅允许自有dApp铸造)实现方案
核心逻辑是「后端授权签名 + 合约校验签名」:你需要在后端维护一个仅自己持有私钥的签名地址,用户在dApp发起铸造时先向后端请求合法签名,调用合约mint方法时必须携带该签名,合约校验签名为授权地址签发才允许铸造。用户直接调用合约时无法获得合法签名,会被直接拦截。
1. Node.js 后端签名服务实现
首先安装ethers依赖:npm install ethers
将签名私钥存在.env环境变量中,禁止硬编码泄露,核心签名逻辑示例:
const { ethers } = require("ethers"); require('dotenv').config(); // 签名私钥,仅后端可访问 const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY; const signer = new ethers.Wallet(SIGNER_PRIVATE_KEY); // 对外提供的签名接口逻辑 async function generateMintSignature(userAddress, tokenId, deadline) { // 构造消息哈希,参数顺序、类型必须和合约完全一致 const messageHash = ethers.utils.solidityKeccak256( ["address", "uint256", "uint256"], [userAddress, tokenId, deadline] ); // 生成符合EIP-191标准的签名,避免恶意签名风险 const signature = await signer.signMessage(ethers.utils.arrayify(messageHash)); return signature; }
2. React 前端交互实现
前端仅负责传递参数、获取签名、调用合约,不要接触签名私钥:
import { ethers } from "ethers"; // 初始化钱包provider const provider = new ethers.providers.Web3Provider(window.ethereum); const userSigner = provider.getSigner(); const NFT_CONTRACT_ADDRESS = "你的合约地址"; const NFT_CONTRACT_ABI = []; // 你的合约ABI const handleMint = async () => { // 获取当前用户钱包地址 const userAddress = await userSigner.getAddress(); // 生成铸造参数,tokenId可根据自己业务逻辑生成 const tokenId = 100; // 签名有效期10分钟,防止签名被长期复用 const deadline = Math.floor(Date.now() / 1000) + 10 * 60; // 请求后端获取签名 const signRes = await fetch("/你的后端签名接口地址", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userAddress, tokenId, deadline }) }); const { signature } = await signRes.json(); // 携带签名调用合约mint方法 const nftContract = new ethers.Contract(NFT_CONTRACT_ADDRESS, NFT_CONTRACT_ABI, userSigner); const mintTx = await nftContract.mint(userAddress, tokenId, deadline, signature); await mintTx.wait(); alert("NFT铸造成功"); }
3. Solidity 合约签名校验实现
使用OpenZeppelin的ECDSA工具库做签名校验,合约示例:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract CustomNFT is ERC721 { using ECDSA for bytes32; // 授权签名地址,部署合约时传入后端签名私钥对应的公钥地址 address public immutable authorizedSigner; // 可选:记录已使用的签名,防止重放攻击 mapping(bytes32 => bool) public usedSignature; constructor(address _authorizedSigner) ERC721("MyNFT", "MNFT") { authorizedSigner = _authorizedSigner; } function mint( address to, uint256 tokenId, uint256 deadline, bytes calldata signature ) external { // 校验签名有效期 require(block.timestamp <= deadline, "签名已过期"); // 构造和后端完全一致的消息哈希 bytes32 msgHash = keccak256(abi.encodePacked(to, tokenId, deadline)); // 匹配EIP-191签名前缀规则 bytes32 ethSignedHash = msgHash.toEthSignedMessageHash(); // 可选:校验签名未被使用过 require(!usedSignature[ethSignedHash], "签名已被使用"); // 恢复签名者地址,校验是否为授权地址 require(ethSignedHash.recover(signature) == authorizedSigner, "签名无效"); // 校验通过,标记签名已使用 usedSignature[ethSignedHash] = true; // 执行铸造 _safeMint(to, tokenId); } }
注意事项
- 后端签名私钥必须严格保密,建议存放在云加密服务或者环境变量中,绝对不能提交到代码仓库、也不能暴露在前端代码中
- 后端、前端、合约三者的签名参数顺序、类型必须完全一致,否则会出现签名校验失败的问题
- 强烈建议同时加deadline有效期和签名使用记录两个规则,避免签名被截获后复用
- 后端签名接口可以叠加自己的业务校验逻辑,比如校验用户是否在白名单、是否已经铸造过、是否符合铸造条件,符合要求才返回签名,从源头控制铸造权限
内容的提问来源于stack exchange,提问作者jasan
相关产品推荐
相关产品推荐

