荷兰拍卖合约
概述
荷兰拍卖合约(DutchAuction
)实现了一个标准的荷兰式拍卖系统,支持 NFT 和 ERC20 代币的拍卖交易。该合约采用价格递减的拍卖机制,起始价格较高,随时间逐步降低,直到买家接受当前价格。
合约实现
// 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 Dutch Auction
* @dev 荷兰式拍卖合约实现
*/
contract DutchAuction is ReentrancyGuard, Pausable, Ownable {
using SafeMath for uint256;
// 拍卖信息结构
struct Auction {
address seller; // 卖家地址
address tokenAddress; // 拍卖物品地址
uint256 tokenId; // 物品ID
uint256 startPrice; // 起始价格
uint256 endPrice; // 最低价格
uint256 duration; // 拍卖时长
uint256 startTime; // 开始时间
uint256 priceStep; // 降价步长
bool ended; // 是否结束
bool isERC721; // 是否为 NFT
}
// 状态变量
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 endPrice
);
event AuctionSuccessful(
uint256 indexed auctionId,
address indexed buyer,
uint256 price
);
event AuctionCancelled(
uint256 indexed auctionId,
address indexed seller
);
event PriceUpdated(
uint256 indexed auctionId,
uint256 newPrice
);
/**
* @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 endPrice,
uint256 duration,
bool isERC721
) external whenNotPaused nonReentrant returns (uint256) {
require(startPrice > endPrice, "Start price must be > end price");
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 priceStep = (startPrice - endPrice) / duration;
auctions[auctionId] = Auction({
seller: msg.sender,
tokenAddress: tokenAddress,
tokenId: tokenId,
startPrice: startPrice,
endPrice: endPrice,
duration: duration,
startTime: block.timestamp,
priceStep: priceStep,
ended: false,
isERC721: isERC721
});
emit AuctionCreated(
auctionId,
msg.sender,
tokenAddress,
tokenId,
startPrice,
endPrice
);
return auctionId;
}
/**
* @dev 购买拍卖物品
*/
function buy(uint256 auctionId) external payable whenNotPaused nonReentrant {
Auction storage auction = auctions[auctionId];
require(!auction.ended, "Auction already ended");
require(block.timestamp <= auction.startTime + auction.duration, "Auction expired");
uint256 currentPrice = getCurrentPrice(auctionId);
require(msg.value >= currentPrice, "Insufficient payment");
auction.ended = true;
// 转移拍卖物品
if (auction.isERC721) {
IERC721(auction.tokenAddress).safeTransferFrom(
auction.seller,
msg.sender,
auction.tokenId
);
} else {
IERC20(auction.tokenAddress).transferFrom(
auction.seller,
msg.sender,
1
);
}
// 计算平台费用
uint256 fee = currentPrice.mul(platformFee).div(10000);
uint256 sellerProceeds = currentPrice.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");
// 退还多余的 ETH
if (msg.value > currentPrice) {
(bool refundSuccess, ) = msg.sender.call{value: msg.value - currentPrice}("");
require(refundSuccess, "Refund failed");
}
emit AuctionSuccessful(auctionId, msg.sender, currentPrice);
}
/**
* @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");
auction.ended = true;
emit AuctionCancelled(auctionId, msg.sender);
}
/**
* @dev 获取当前价格
*/
function getCurrentPrice(uint256 auctionId) public view returns (uint256) {
Auction storage auction = auctions[auctionId];
require(!auction.ended, "Auction ended");
uint256 elapsed = block.timestamp - auction.startTime;
if (elapsed >= auction.duration) {
return auction.endPrice;
}
uint256 priceDrop = auction.priceStep.mul(elapsed);
uint256 currentPrice = auction.startPrice.sub(priceDrop);
return currentPrice > auction.endPrice ? currentPrice : auction.endPrice;
}
/**
* @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. 创建拍卖
const startPrice = ethers.utils.parseEther("2"); // 起始价格 2 ETH
const endPrice = ethers.utils.parseEther("0.5"); // 最低价格 0.5 ETH
const duration = 3600 * 24; // 持续时间 24 小时
await dutchAuction.createAuction(
nft.address, // NFT 合约地址
tokenId, // NFT ID
startPrice,
endPrice,
duration,
true // 是 NFT
);
2. 购买物品
const currentPrice = await dutchAuction.getCurrentPrice(auctionId);
await dutchAuction.buy(auctionId, {
value: currentPrice
});
3. 取消拍卖
await dutchAuction.cancelAuction(auctionId);
最佳实践
1. 拍卖设置
- 合理的起始价格和最低价格
- 适当的拍卖持续时间
- 考虑市场流动性
2. 安全考虑
- 定期检查合约状态
- 监控异常交易
- 做好应急预案
3. 费用管理
- 合理的平台费用比例
- 安全的费用接收地址
- 及时的费用提取
总结
该荷兰拍卖合约实现了:
- 完整的拍卖功能
- 灵活的价格机制
- 严格的安全控制
- 可靠的资金管理
通过这个实现,可以安全、高效地进行 NFT 和代币的荷兰式拍卖交易。
常见问题解答(FAQ)
1. 基本概念
Q: 什么是荷兰式拍卖? A: 荷兰式拍卖是一种价格递减的拍卖机制,拍卖从较高的起始价格开始,随时间逐渐降低,直到第一个买家愿意以当前价格购买。这种机制特别适合需要快速售出的场景。
Q: 荷兰式拍卖与英式拍卖有什么区别? A: 主要区别在于价格变动方向和成交机制。荷兰式拍卖价格递减,第一个接受价格的买家直接成交;而英式拍卖价格递增,通过多轮竞价确定最终买家。
2. 功能相关
Q: 如何确定合适的起始价格和最低价格? A: 建议参考以下因素:
- 市场当前价格水平
- 目标售出时间
- 历史交易数据
- 潜在买家的购买力
- 类似资产的价格趋势
Q: 价格如何随时间递减? A: 价格递减采用线性递减方式,通过以下公式计算:
currentPrice = startPrice - (startPrice - endPrice) * elapsedTime / duration
Q: 如何处理提前结束的拍卖? A: 卖家可以通过调用 cancelAuction
函数取消未成交的拍卖。但一旦有人购买,拍卖就会立即结束并完成交易。
3. 安全相关
Q: 如何防止价格操纵? A: 合约采取以下措施:
- 严格的价格递减算法
- 交易前的余额和授权检查
- 防重入保护
- 交易原子性保证
Q: 如果交易失败了怎么办? A: 合约包含以下保护机制:
- 所有状态更改都在最后执行
- 交易失败会自动回滚
- 买家的资金会立即退回
- 提供紧急暂停功能
4. 费用相关
Q: 平台费用如何计算? A: 平台费用通过以下方式计算:
fee = price * platformFee / 10000 // platformFee 以基点(0.01%)为单位
Q: 卖家最终能收到多少钱? A: 卖家收到的金额计算公式:
sellerProceeds = finalPrice - platformFee
5. 技术相关
Q: 为什么要使用 SafeMath? A: SafeMath 库用于防止数值计算溢出,虽然 Solidity 0.8.0 以上版本内置了溢出检查,但使用 SafeMath 可以提供更好的兼容性和可读性。
Q: 如何处理不同代币类型? A: 合约通过 isERC721
标志区分 NFT 和 ERC20 代币,并相应地调用不同的转账接口。对于 NFT,使用 safeTransferFrom
;对于 ERC20,使用 transferFrom
。
6. 最佳实践
Q: 创建拍卖时需要注意什么? A: 建议注意以下几点:
- 确保资产已授权给合约
- 设置合理的价格区间
- 选择适当的拍卖时长
- 考虑市场活跃度
- 预估潜在买家数量
Q: 如何监控拍卖进展? A: 可以通过以下方式:
- 监听合约事件
- 定期查询当前价格
- 跟踪参与者活动
- 使用前端工具实时展示
- 设置价格提醒
7. 错误处理
Q: 常见的错误码和解决方案? A: 主要错误码说明:
"Not token owner"
: 确认是否拥有代币"Not approved"
: 检查授权状态"Duration too short/long"
: 调整拍卖时长"Price must be > end price"
: 修改价格设置"Auction already ended"
: 检查拍卖状态
Q: 如何处理异常情况? A: 合约提供以下机制:
- 紧急暂停功能
- 取消拍卖选项
- 管理员干预接口
- 资金安全保护
- 状态回滚机制
8. 升级和维护
Q: 合约是否支持升级? A: 当前版本不支持直接升级,但可以:
- 部署新版本合约
- 迁移未完成的拍卖
- 保留历史数据访问
- 实现平滑过渡方案
Q: 如何进行合约维护? A: 建议采取以下措施:
- 定期检查合约状态
- 监控异常交易
- 更新安全参数
- 维护白名单
- 优化费用配置