4. 智能合约层设计:合约架构、ERC-721与ERC-1155标准、拍卖合约核心逻辑、安全考量

好,咱们进入智能合约层。这一层是整个NFT拍卖系统的核心,说白了就是「链上的法律条文」。你写的每一行代码,都会直接影响用户的钱包和资产。我见过太多项目因为合约写得糙,上线就被黑客当提款机。所以这一章,咱们把合约架构、标准选择、核心逻辑和安全考量,一次性讲透。

4.1 合约架构:分层与职责

我个人习惯把合约拆成三层,这样后期维护和升级都方便。你想想看,如果所有逻辑都塞在一个合约里,改个参数就得重新部署,用户还得迁移资产,多麻烦。

  • 数据层:负责存储NFT元数据、拍卖状态、出价记录。我建议用独立的Storage合约,方便后续升级。
  • 逻辑层:处理拍卖的核心流程——创建拍卖、出价、结算、取消。这一层不直接操作存储,而是通过接口调用数据层。
  • 接口层:对外暴露ERC-721或ERC-1155标准接口,以及拍卖相关的函数。用户和前端都跟这一层打交道。

重要:分层之后,逻辑合约可以升级,但数据合约保持稳定。这样用户的NFT不会因为升级而丢失。

我在项目中遇到过一种情况:团队把拍卖逻辑和NFT铸造写在一个合约里,结果发现拍卖逻辑有bug,不得不硬分叉。嗯,从那以后我再也不这么干了。

4.2 ERC-721 vs ERC-1155:怎么选?

这两个标准是NFT的基石。ERC-721是「非同质化代币」,每个token都是独一无二的。ERC-1155是「半同质化代币」,一个合约可以管理多种资产。

特性 ERC-721 ERC-1155
唯一性 每个token ID唯一 同一ID可批量发行
Gas成本 单次操作较高 批量操作更省Gas
适用场景 数字艺术品、收藏品 游戏道具、门票、多版本NFT
合约复杂度 简单 稍复杂,需管理多种类型

我建议:如果你的项目是「一件一件卖」的稀缺艺术品,用ERC-721。如果是游戏里的武器、皮肤,或者同一幅画有多个版次,用ERC-1155更划算。说白了,ERC-1155就是给「批量生产」准备的。

小技巧:ERC-1155支持安全转账函数safeTransferFrom,记得用这个,别用transferFrom。我曾经见过有人用transferFrom把NFT转到了黑洞地址,再也找不回来。

4.3 拍卖合约核心逻辑

拍卖合约的核心就三个动作:创建拍卖、出价、结算。咱们一个一个拆。

4.3.1 创建拍卖

卖家调用createAuction函数,传入NFT地址、token ID、起拍价、持续时间。合约会检查卖家是否授权了该NFT,然后锁定资产。

function createAuction(
    address nftContract,
    uint256 tokenId,
    uint256 startingPrice,
    uint256 duration
) external {
    // 检查授权
    IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
    // 存储拍卖信息
    auctions[auctionId] = Auction({
        seller: msg.sender,
        nftContract: nftContract,
        tokenId: tokenId,
        startingPrice: startingPrice,
        highestBid: 0,
        highestBidder: address(0),
        endTime: block.timestamp + duration,
        ended: false
    });
}

这里有个坑:一定要先检查授权再转账。我曾经见过一个合约,先转账再检查,结果用户没授权也能创建拍卖,资产直接锁死在合约里。

4.3.2 出价逻辑

买家调用bid函数,传入拍卖ID和出价金额。合约会检查出价是否高于当前最高价,并且拍卖还没结束。

function bid(uint256 auctionId) external payable {
    Auction storage auction = auctions[auctionId];
    require(block.timestamp < auction.endTime, "Auction ended");
    require(msg.value > auction.highestBid, "Bid too low");

    // 退还上一个最高出价者
    if (auction.highestBidder != address(0)) {
        payable(auction.highestBidder).transfer(auction.highestBid);
    }

    auction.highestBid = msg.value;
    auction.highestBidder = msg.sender;
}

警告:退还出价时,别用transfer,用call。因为transfer有2300 gas限制,如果接收方是合约,可能会失败。我建议用 (bool success, ) = payable(addr).call{value: amount}(""); 这种模式。

4.3.3 结算逻辑

拍卖结束后,卖家或任何人都可以调用settleAuction。合约会把NFT转给最高出价者,把ETH转给卖家。

function settleAuction(uint256 auctionId) external {
    Auction storage auction = auctions[auctionId];
    require(block.timestamp >= auction.endTime, "Auction not ended");
    require(!auction.ended, "Already settled");

    auction.ended = true;
    // 转NFT给买家
    IERC721(auction.nftContract).transferFrom(
        address(this),
        auction.highestBidder,
        auction.tokenId
    );
    // 转ETH给卖家
    payable(auction.seller).transfer(auction.highestBid);
}

嗯,这里要注意:一定要先标记ended为true,再执行转账。防止重入攻击。你想想看,如果先转账再标记,攻击者可以在转账回调里再次调用settleAuction,把同一笔钱提两次。

4.4 安全考量:血的教训

安全是智能合约的生命线。我整理了几个最常见的坑,你写代码时一定要避开。

  • 重入攻击:上面提到了,用「检查-生效-交互」模式。先更新状态,再转账。
  • 整数溢出:Solidity 0.8+ 默认有溢出检查,但如果你用旧版本,记得加SafeMath。
  • 时间戳操纵:矿工可以微调block.timestamp,但一般影响不大。别用它做随机数生成。
  • 前端跑单:用户看到拍卖快结束时出价,但交易可能被矿工延迟。我建议加一个「延长时间」机制,最后5分钟有人出价,自动延长5分钟。

核心原则:永远不要信任外部输入。所有数据都要在链上验证。我曾经见过一个项目,直接从前端传价格过来,结果用户传了个0,白嫖了NFT。

最后说一句:写合约之前,先画好状态机。把每个状态之间的转换条件写清楚。这样不容易漏掉边界情况。好了,这一章就到这里,下一章咱们聊聊「链下服务与事件监听」。