Skip to content

英式拍卖合约

概述

英式拍卖合约(EnglishAuction)实现了一个标准的英式拍卖系统,支持 NFT 和 ERC20 代币的拍卖交易。该合约采用价格递增的拍卖机制,参与者可以不断提高出价,直到拍卖结束时最高出价者获得拍卖品。

合约实现

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

/**
 * @title English Auction
 * @dev 英式拍卖合约实现
 */
contract EnglishAuction is ReentrancyGuard, Pausable, Ownable {
    using SafeMath for uint256;

    // 拍卖信息结构
    struct Auction {
        address seller;          // 卖家地址
        address tokenAddress;    // 拍卖物品地址
        uint256 tokenId;        // 物品ID
        uint256 startPrice;     // 起始价格
        uint256 reservePrice;   // 保留价格
        uint256 minIncrement;   // 最小加价幅度
        uint256 duration;       // 拍卖时长
        uint256 startTime;      // 开始时间
        uint256 endTime;        // 结束时间
        address highestBidder;  // 最高出价者
        uint256 highestBid;     // 最高出价
        bool ended;             // 是否结束
        bool isERC721;          // 是否为 NFT
        mapping(address => uint256) pendingReturns; // 待退还的出价
    }

    // 状态变量
    mapping(uint256 => Auction) public auctions;
    uint256 public auctionIdCounter;
    uint256 public minAuctionDuration;
    uint256 public maxAuctionDuration;
    uint256 public platformFee;
    address public feeRecipient;

    // 事件定义
    event AuctionCreated(
        uint256 indexed auctionId,
        address indexed seller,
        address tokenAddress,
        uint256 tokenId,
        uint256 startPrice,
        uint256 reservePrice
    );

    event BidPlaced(
        uint256 indexed auctionId,
        address indexed bidder,
        uint256 amount
    );

    event AuctionEnded(
        uint256 indexed auctionId,
        address indexed winner,
        uint256 amount
    );

    event AuctionCancelled(
        uint256 indexed auctionId,
        address indexed seller
    );

    /**
     * @dev 构造函数
     */
    constructor(
        uint256 _minDuration,
        uint256 _maxDuration,
        uint256 _platformFee,
        address _feeRecipient
    ) {
        require(_minDuration > 0, "Min duration must be > 0");
        require(_maxDuration > _minDuration, "Max duration must be > min duration");
        require(_platformFee <= 1000, "Platform fee must be <= 10%");
        require(_feeRecipient != address(0), "Invalid fee recipient");

        minAuctionDuration = _minDuration;
        maxAuctionDuration = _maxDuration;
        platformFee = _platformFee;
        feeRecipient = _feeRecipient;
    }

    /**
     * @dev 创建拍卖
     */
    function createAuction(
        address tokenAddress,
        uint256 tokenId,
        uint256 startPrice,
        uint256 reservePrice,
        uint256 minIncrement,
        uint256 duration,
        bool isERC721
    ) external whenNotPaused nonReentrant returns (uint256) {
        require(startPrice > 0, "Start price must be > 0");
        require(reservePrice >= startPrice, "Reserve price must be >= start price");
        require(minIncrement > 0, "Min increment must be > 0");
        require(duration >= minAuctionDuration, "Duration too short");
        require(duration <= maxAuctionDuration, "Duration too long");

        if (isERC721) {
            IERC721 nft = IERC721(tokenAddress);
            require(nft.ownerOf(tokenId) == msg.sender, "Not token owner");
            require(nft.isApprovedForAll(msg.sender, address(this)) || 
                   nft.getApproved(tokenId) == address(this), "Not approved");
        } else {
            IERC20 token = IERC20(tokenAddress);
            require(token.balanceOf(msg.sender) >= 1, "Insufficient token balance");
            require(token.allowance(msg.sender, address(this)) >= 1, "Not approved");
        }

        uint256 auctionId = auctionIdCounter++;
        uint256 endTime = block.timestamp + duration;

        Auction storage auction = auctions[auctionId];
        auction.seller = msg.sender;
        auction.tokenAddress = tokenAddress;
        auction.tokenId = tokenId;
        auction.startPrice = startPrice;
        auction.reservePrice = reservePrice;
        auction.minIncrement = minIncrement;
        auction.duration = duration;
        auction.startTime = block.timestamp;
        auction.endTime = endTime;
        auction.isERC721 = isERC721;

        emit AuctionCreated(
            auctionId,
            msg.sender,
            tokenAddress,
            tokenId,
            startPrice,
            reservePrice
        );

        return auctionId;
    }

    /**
     * @dev 出价
     */
    function bid(uint256 auctionId) external payable whenNotPaused nonReentrant {
        Auction storage auction = auctions[auctionId];
        require(!auction.ended, "Auction already ended");
        require(block.timestamp <= auction.endTime, "Auction expired");
        require(msg.sender != auction.seller, "Seller cannot bid");

        uint256 newBid = msg.value;
        if (auction.highestBidder != address(0)) {
            require(newBid >= auction.highestBid.add(auction.minIncrement), 
                    "Bid increment too low");
            auction.pendingReturns[auction.highestBidder] = auction.pendingReturns[auction.highestBidder].add(auction.highestBid);
        } else {
            require(newBid >= auction.startPrice, "Bid below start price");
        }

        auction.highestBidder = msg.sender;
        auction.highestBid = newBid;

        emit BidPlaced(auctionId, msg.sender, newBid);

        // 如果距离结束不到5分钟,延长拍卖时间
        if (auction.endTime - block.timestamp < 5 minutes) {
            auction.endTime = block.timestamp + 5 minutes;
        }
    }

    /**
     * @dev 结束拍卖
     */
    function endAuction(uint256 auctionId) external nonReentrant {
        Auction storage auction = auctions[auctionId];
        require(!auction.ended, "Auction already ended");
        require(block.timestamp > auction.endTime, "Auction still active");

        auction.ended = true;

        if (auction.highestBidder != address(0) && 
            auction.highestBid >= auction.reservePrice) {
            // 转移拍卖物品
            if (auction.isERC721) {
                IERC721(auction.tokenAddress).safeTransferFrom(
                    auction.seller,
                    auction.highestBidder,
                    auction.tokenId
                );
            } else {
                IERC20(auction.tokenAddress).transferFrom(
                    auction.seller,
                    auction.highestBidder,
                    1
                );
            }

            // 计算平台费用
            uint256 fee = auction.highestBid.mul(platformFee).div(10000);
            uint256 sellerProceeds = auction.highestBid.sub(fee);

            // 分配资金
            if (fee > 0) {
                (bool feeSuccess, ) = feeRecipient.call{value: fee}("");
                require(feeSuccess, "Fee transfer failed");
            }

            (bool success, ) = auction.seller.call{value: sellerProceeds}("");
            require(success, "Seller payment failed");

            emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid);
        } else {
            // 如果没有达到保留价,退还最高出价
            if (auction.highestBidder != address(0)) {
                auction.pendingReturns[auction.highestBidder] = auction.pendingReturns[auction.highestBidder].add(auction.highestBid);
            }
            emit AuctionEnded(auctionId, address(0), 0);
        }
    }

    /**
     * @dev 取消拍卖
     */
    function cancelAuction(uint256 auctionId) external nonReentrant {
        Auction storage auction = auctions[auctionId];
        require(msg.sender == auction.seller, "Not auction seller");
        require(!auction.ended, "Auction already ended");
        require(auction.highestBidder == address(0), "Bids already placed");

        auction.ended = true;
        emit AuctionCancelled(auctionId, msg.sender);
    }

    /**
     * @dev 提取退还的出价
     */
    function withdraw() external nonReentrant {
        uint256 amount = 0;
        for (uint256 i = 0; i < auctionIdCounter; i++) {
            amount = amount.add(auctions[i].pendingReturns[msg.sender]);
            auctions[i].pendingReturns[msg.sender] = 0;
        }
        require(amount > 0, "No funds to withdraw");

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Withdrawal failed");
    }

    /**
     * @dev 查询待退还金额
     */
    function getPendingReturns(
        uint256 auctionId,
        address bidder
    ) external view returns (uint256) {
        return auctions[auctionId].pendingReturns[bidder];
    }

    /**
     * @dev 更新平台费用
     */
    function updatePlatformFee(uint256 _platformFee) external onlyOwner {
        require(_platformFee <= 1000, "Platform fee must be <= 10%");
        platformFee = _platformFee;
    }

    /**
     * @dev 更新费用接收地址
     */
    function updateFeeRecipient(address _feeRecipient) external onlyOwner {
        require(_feeRecipient != address(0), "Invalid fee recipient");
        feeRecipient = _feeRecipient;
    }

    /**
     * @dev 更新拍卖时长限制
     */
    function updateDurationLimits(
        uint256 _minDuration,
        uint256 _maxDuration
    ) external onlyOwner {
        require(_minDuration > 0, "Min duration must be > 0");
        require(_maxDuration > _minDuration, "Max duration must be > min duration");
        minAuctionDuration = _minDuration;
        maxAuctionDuration = _maxDuration;
    }

    /**
     * @dev 紧急暂停
     */
    function pause() external onlyOwner {
        _pause();
    }

    /**
     * @dev 恢复运行
     */
    function unpause() external onlyOwner {
        _unpause();
    }

    /**
     * @dev 接收 NFT 回调
     */
    function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

功能说明

1. 拍卖管理

  • 创建拍卖:支持 NFT 和 ERC20 代币的拍卖
  • 设置参数:起始价格、保留价格、最小加价幅度
  • 取消拍卖:在无人出价时可以取消
  • 平台费用:支持收取交易费用

2. 出价机制

  • 最小加价:确保每次加价幅度合理
  • 自动延期:临近结束时自动延长
  • 保留价格:设置最低成交价格
  • 出价退还:未中标者可提取出价

3. 拍卖结束

  • 自动结算:完成代币转移和资金分配
  • 保留价验证:检查是否达到保留价
  • 退款处理:未达到保留价时退还出价
  • 平台分成:自动扣除平台费用

4. 安全机制

  • 重入保护:防止重入攻击
  • 暂停功能:紧急情况可暂停合约
  • 权限控制:管理功能仅限所有者
  • 参数验证:严格的输入参数检查

使用示例

1. 创建拍卖

javascript
const startPrice = ethers.utils.parseEther("1");     // 起始价格 1 ETH
const reservePrice = ethers.utils.parseEther("2");   // 保留价格 2 ETH
const minIncrement = ethers.utils.parseEther("0.1"); // 最小加价 0.1 ETH
const duration = 3600 * 24;                          // 持续时间 24 小时

await englishAuction.createAuction(
    nft.address,      // NFT 合约地址
    tokenId,          // NFT ID
    startPrice,
    reservePrice,
    minIncrement,
    duration,
    true              // 是 NFT
);

2. 出价

javascript
const bidAmount = ethers.utils.parseEther("1.5");
await englishAuction.bid(auctionId, {
    value: bidAmount
});

3. 结束拍卖

javascript
await englishAuction.endAuction(auctionId);

4. 提取退还的出价

javascript
await englishAuction.withdraw();

最佳实践

1. 拍卖设置

  • 合理的起始价格和保留价格
  • 适当的最小加价幅度
  • 充足的拍卖时间

2. 安全考虑

  • 定期检查合约状态
  • 监控异常交易
  • 做好应急预案

3. 费用管理

  • 合理的平台费用比例
  • 安全的费用接收地址
  • 及时的费用提取

总结

该英式拍卖合约实现了:

  • 完整的拍卖功能
  • 灵活的出价机制
  • 严格的安全控制
  • 可靠的资金管理

通过这个实现,可以安全、高效地进行 NFT 和代币的英式拍卖交易。

常见问题解答(FAQ)

1. 基本概念

Q: 什么是英式拍卖? A: 英式拍卖是一种价格递增的拍卖机制,参与者通过多轮出价竞争,价格从起拍价开始上涨,最终由出价最高者获得拍卖品。这是最传统和常见的拍卖形式。

Q: 英式拍卖与荷兰式拍卖有什么区别? A: 主要区别在于:

  • 价格变动:英式拍卖价格递增,荷兰式拍卖价格递减
  • 竞价方式:英式拍卖需要多轮竞价,荷兰式拍卖一次性成交
  • 成交时间:英式拍卖时间较长,荷兰式拍卖相对较快
  • 竞争程度:英式拍卖更能反映市场真实需求

2. 功能相关

Q: 如何设置合理的起拍价? A: 建议考虑以下因素:

  • 资产的市场估值
  • 历史成交价格
  • 当前市场情况
  • 潜在买家数量
  • 拍卖时间长度

Q: 最小加价幅度如何确定? A: 最小加价幅度应该:

  • 与拍品价值相匹配
  • 考虑市场流动性
  • 平衡竞价效率
  • 避免恶意竞价
  • 适应市场波动

Q: 延期机制是如何工作的? A: 当接近结束时收到新出价,拍卖会自动延期:

solidity
if (block.timestamp >= auction.endTime - 5 minutes) {
    auction.endTime = block.timestamp + 5 minutes;
}

3. 安全相关

Q: 如何防止虚假出价? A: 合约采取以下措施:

  • 要求出价者提供保证金
  • 验证出价者余额
  • 检查出价合理性
  • 记录出价历史
  • 实施惩罚机制

Q: 如何保护参与者权益? A: 通过以下机制:

  • 资金托管保护
  • 自动退还出价
  • 透明的价格信息
  • 可靠的状态管理
  • 完整的事件记录

4. 费用相关

Q: 保证金如何管理? A: 保证金管理机制:

  • 出价时锁定保证金
  • 更高出价自动释放前一出价的保证金
  • 拍卖结束后退还未中标者保证金
  • 中标者保证金转为部分付款

Q: 平台费用如何收取? A: 平台费用计算方式:

solidity
platformFee = finalPrice * feeRate / 10000  // feeRate 以基点(0.01%)为单位

5. 技术相关

Q: 如何处理并发出价? A: 并发控制措施:

  • 使用区块时间戳
  • 严格的状态检查
  • 原子性交易
  • 防重入保护
  • 顺序性保证

Q: 如何确保价格更新的准确性? A: 通过以下机制:

  • 严格的价格验证
  • 状态一致性检查
  • 事务原子性
  • 错误自动回滚
  • 完整的日志记录

6. 最佳实践

Q: 参与拍卖需要注意什么? A: 建议注意:

  • 检查拍卖状态
  • 确认资金充足
  • 了解竞价规则
  • 评估市场价值
  • 设置价格提醒

Q: 如何提高拍卖成功率? A: 可以采取以下措施:

  • 选择合适的起拍价
  • 设置合理的拍卖时长
  • 充分的市场推广
  • 透明的拍卖信息
  • 良好的用户体验

7. 错误处理

Q: 常见错误及解决方案? A: 主要错误类型:

  • "Auction not active": 检查拍卖状态
  • "Bid too low": 提高出价金额
  • "Insufficient balance": 确保资金充足
  • "Not authorized": 检查授权状态
  • "Invalid timing": 验证操作时间

Q: 如何处理异常终止? A: 异常处理机制:

  • 紧急暂停功能
  • 资金安全保护
  • 状态恢复选项
  • 管理员干预
  • 用户赔偿方案

8. 升级和维护

Q: 如何优化拍卖性能? A: 性能优化建议:

  • 批量处理操作
  • 优化数据结构
  • 减少状态存储
  • 使用事件代替存储
  • 实现高效查询

Q: 如何进行数据分析? A: 数据分析方法:

  • 监控拍卖活动
  • 分析价格趋势
  • 跟踪用户行为
  • 评估市场表现
  • 生成统计报告

Released under the MIT License by Vogeb.