Skip to main content

目录

  1. 荷兰式拍卖
  2. 秘密竞拍
  3. 远程购买

常见四大拍卖形式 拍卖方式起源

目前主要的拍卖方式有以下四种:

1、英式拍卖

也称为“出价逐升式拍卖”,是目前最流行的网上拍卖方式。拍卖中,竞买人出价由低开始,此后出价一个比前一个要高,直到没有更高的出价为止,出价最高即最后一个竞买人将以其所出的价格获得该商品。

传统的和网上的英式拍卖在做法上有所不同。传统拍卖中,对每件拍卖品来说,不需要事先确定拍卖时间,一般数分钟即可结束;而网上拍卖则需要事先确定拍卖的起止时间,一般是数日或数周。

英式拍卖对卖方和竞买人来说都有缺点。既然获胜的竞买人的出价只需比前一个最高价高一点,那么每个竞买人都不愿马上按照其预告价出价。当然,竞买人也要冒风险,他可能会被令人兴奋的竞价过程吸引,出价超出了预估价,这种心理现象被称为赢者诅咒(Winner’s Curse)。

2、荷兰式拍卖

是英式拍卖的逆行,也称为“出价逐降式拍卖”。它是先由拍卖人给出一个潜在的最高价,然后价格不断下降,直到有人接受价格。荷兰式拍卖成交的速度特别快,经常用来拍卖诸如果蔬、食品之类的不易长期保存的鲜活产品。如果拍卖的是同类多件物品,竞买人一般会随着价格的下降而增多,拍卖过程一直进行到拍卖品的供应量与总需求量相等为止。还有的拍卖站点,出价最高者也可以以出价最低的获胜的竞买人的价格获得该产品。

该方式的缺点是拍卖速度太快,而且需求所有竞买人在某一时候竞买。

在荷兰拍卖(Dutch Auction)中,"以当前价格出价" 意味着买家同意以拍卖中目前的价格购买拍卖品。荷兰拍卖的特点是拍卖价格从一个较高的起始价格开始,并且随着时间的推移逐渐降低,直到有买家接受当前价格并出价,或者价格降到一个预设的最低价格为止。

这个过程如下:

  1. 拍卖开始:卖方或拍卖师设定一个起始价格,这通常是他们希望收到的最高价格。
  2. 价格下降:如果起始价格没有吸引到买家,价格会按照预定的规则降低,例如,每过一定时间价格降低一定的百分比或固定金额。
  3. 买家决定:买家在拍卖过程中观察价格的变化。当价格降到买家认为合理或者他们愿意支付的水平时,他们可以决定出价。
  4. 出价:买家通过支付当前的拍卖价格来出价,这意味着他们同意按照这个价格购买拍卖品。
  5. 拍卖结束:一旦有买家出价,拍卖立即结束,该买家以他们出价的价格获得拍卖品。

"以当前价格出价" 是买家表明他们愿意立刻按照拍卖中现有的价格购买物品的行为。在实际操作中,这通常意味着买家必须密切关注拍卖价格,并在价格降到他们满意的水平时迅速作出反应。在线上或智能合约驱动的荷兰拍卖中,这通常通过发送一个交易来完成,该交易包含了等于或高于当前拍卖价格的金额。

3、密封拍卖(Sealed Auction)

是指竞买人通过加密的 E-mail 将出价发送给拍卖人,再由拍卖人统一开标后,比较各方递价,最后确定中标人。

网上密封拍卖多用于工程项目、大宗货物、土地房产等不动产交易以及资源开采权出让等交易。目前,这种拍卖方式已被越来越多国家政府用于在网上销售库存物资以及海关处理的货物。

密封拍卖可分为一级密封拍卖和二级密封拍卖。一级密封拍卖也称为密封递价最高价拍卖,即在密封递价过程中,出价最高的竞买人中标。如果拍卖的是多件相同物品,出价低于前一个的竞买人购得剩余的拍卖品。二级密封拍卖也称为密封递价次高价拍卖,其递价过程与一及密封拍卖类似,只是出价最高的竞买人是按照出价第二高的竞买人所出的价格都按其预告价出价,降低了竞买人串通的可能性,获胜者不必按照最高价付款,从而使所有的竞买人都想以比其一级密封拍卖中高一些的价格出价。威廉.维克瑞(William Vickrey)因对此拍卖的研究而荣获 1996 年诺贝尔经济学奖,因此,二级密封拍卖也称为维氏拍卖。

4、双重拍卖

该方式是买方和卖方同时递交价格和数量来出价。在网上双重拍卖中,买方和卖方出价是通过软件代理竞价系统进行的。拍卖开始前,买方向软件代理竞价系统提交最低出价和出价增量,卖方向软件代理竞价系统提交最高要价和要价减量。网上拍卖信息系统把卖方的要约和买方的要约进行匹配,直到要约提出的所有出售数量都卖给了买方。双重拍卖只对那些事先知道质量的物品有效。例如,有价有标准级别的农副产品,通常这类物品交易的数量很大。网上双重拍卖既可按照公开出价方式也可按照密封递价方式进行。

目前国内外的拍卖网站,其竞价模式实际上只有两种,即正向竞价和逆向竞价;其交易方式则有三种:竞价拍卖、竞价拍买和集体议价。

荷兰式拍卖 以当前价格出价

为了增强合约的安全性和处理边缘情况,我们可以添加一些功能和检查。以下是更新后的荷兰拍卖合约代码,它包括了一些额外的安全措施,如重入攻击防护、合理的状态变量检查、事件记录以及资金和代币的安全转移。

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

interface IERC20 {
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}

contract DutchAuction {
IERC20 public auctionToken;
address public seller;
uint256 public startPrice;
uint256 public endPrice;
uint256 public priceDecrement;
uint256 public startTime;
uint256 public endTime;
uint256 public totalTokens;
bool public auctionEnded;
event AuctionStarted(uint256 indexed startTime, uint256 indexed endTime);
event TokensPurchased(address indexed buyer, uint256 amountSpent, uint256 tokensBought);
event AuctionEnded(uint256 finalPrice);
event FundsWithdrawn(address indexed seller, uint256 amount);
event UnsoldTokensWithdrawn(address indexed seller, uint256 tokens);

modifier onlySeller() {
require(msg.sender == seller, "Only seller can call this.");
_;
}

modifier auctionActive() {
require(startTime != 0 && block.timestamp >= startTime, "Auction not started.");
require(block.timestamp < endTime, "Auction ended.");
_;
}

modifier auctionEndedSuccessfully() {
require(auctionEnded, "Auction has not ended yet.");
_;
}

constructor(
address _tokenAddress,
uint256 _startPrice,
uint256 _endPrice,
uint256 _duration,
uint256 _totalTokens
) {
require(_startPrice > _endPrice, "Start price must be higher than end price.");
require(_duration > 0, "Duration must be positive.");
require(_totalTokens > 0, "Total tokens must be positive.");

auctionToken = IERC20(_tokenAddress);
seller = msg.sender;
startPrice = _startPrice;
endPrice = _endPrice;
priceDecrement = (_startPrice - _endPrice) / _duration;
totalTokens = _totalTokens;
}

function startAuction() external onlySeller {
require(startTime == 0, "Auction already started.");
require(auctionToken.balanceOf(address(this)) >= totalTokens, "Insufficient tokens for auction.");

startTime = block.timestamp;
endTime = startTime + (startPrice - endPrice) / priceDecrement;
emit AuctionStarted(startTime, endTime);
}

function buyTokens() external payable auctionActive {
uint256 currentPrice = getCurrentPrice();
uint256 tokensToBuy = msg.value / currentPrice;
require(tokensToBuy > 0 && tokensToBuy <= totalTokens, "Invalid purchase amount.");

totalTokens -= tokensToBuy;
emit TokensPurchased(msg.sender, msg.value, tokensToBuy);

// Use a check-effects-interactions pattern to prevent re-entrancy
auctionToken.transfer(msg.sender, tokensToBuy);
}

function endAuction() public auctionActive {
if (totalTokens == 0 || block.timestamp >= endTime) {
auctionEnded = true;
emit AuctionEnded(getCurrentPrice());
}
}

function getCurrentPrice() public view returns (uint256) {
if (block.timestamp >= endTime) {
return endPrice;
} else {
uint256 timeElapsed = block.timestamp - startTime;
uint256 priceReduction = priceDecrement * timeElapsed;
return startPrice - priceReduction;
}
}

function withdrawFunds() external onlySeller auctionEndedSuccessfully {
uint256 funds = address(this).balance;
require(funds > 0, "No funds to withdraw.");

emit FundsWithdrawn(seller, funds);
payable(seller).transfer(funds);
}

function withdrawUnsoldTokens() external onlySeller auctionEndedSuccessfully {
if (totalTokens > 0) {
uint256 unsoldTokens = totalTokens;
totalTokens = 0;

emit UnsoldTokensWithdrawn(seller, unsoldTokens);
auctionToken.transfer(seller, unsoldTokens);
}
}
}

改进点:

  1. 重入攻击防护:使用了 check-effects-interactions 模式,在buyTokens函数中先减少totalTokens的数量,然后触发事件,最后进行代币转移。

  2. 状态变量检查:增加了对拍卖状态的检查,确保拍卖在正确的阶段进行。

  3. 事件记录:增加了更多的事件,以便更好地跟踪拍卖的状态变化。

  4. 安全转移:在提取资金和未售出代币的函数中增加了额外的检查,确保操作的合法性。

  5. 拍卖结束逻辑:添加了endAuction函数,以手动或自动触发拍卖结束,设置auctionEnded状态变量,并触发AuctionEnded事件。

在部署之前,请确保对合约进行充分的测试,并考虑进行专业的安全审计,以确保代码的安全性和健壮性。

NFT 合约

了解了您的需求后,下面是一个完整的简单荷兰式拍卖合约示例,用于拍卖单个 ERC721 NFT。这个合约包括了开始拍卖、购买 NFT、结束拍卖、提取资金以及在拍卖结束后未售出时取回 NFT 的功能。

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

// 简单的ERC721代币接口
interface IERC721 {
function transferFrom(address from, address to, uint256 tokenId) external;
function ownerOf(uint256 tokenId) external view returns (address);
}

contract DutchAuctionNFT {
IERC721 public nft; // NFT合约
uint256 public nftId; // 被拍卖的NFT的ID
address public seller; // 卖家的地址
uint256 public startPrice; // 拍卖开始的价格
uint256 public endPrice; // 拍卖结束的价格
uint256 public priceDecrement; // 每秒价格下降的金额
uint256 public startTime; // 拍卖开始的时间戳
uint256 public endTime; // 拍卖结束的时间戳
bool public auctionEnded; // 拍卖是否结束的标志

event AuctionStarted(uint256 indexed startTime, uint256 indexed endTime);
event NFTPurchased(address indexed buyer, uint256 price);
event AuctionEnded(uint256 finalPrice);
event FundsWithdrawn(address indexed seller, uint256 amount);
event NFTWithdrawn(address indexed seller, uint256 tokenId);

modifier onlySeller() {
require(msg.sender == seller, "Only seller can call this.");
_;
}

modifier auctionActive() {
require(startTime != 0 && block.timestamp >= startTime, "Auction not started.");
require(block.timestamp < endTime && !auctionEnded, "Auction ended.");
_;
}

constructor(
address _nftAddress,
uint256 _nftId,
uint256 _startPrice,
uint256 _endPrice,
uint256 _duration
) {
require(_startPrice > _endPrice, "Start price must be higher than end price.");
require(_duration > 0, "Duration must be positive.");

nft = IERC721(_nftAddress);
nftId = _nftId;
seller = msg.sender;
startPrice = _startPrice;
endPrice = _endPrice;
priceDecrement = (_startPrice - _endPrice) / _duration;
}

function startAuction() external onlySeller {
require(startTime == 0, "Auction already started.");
require(nft.ownerOf(nftId) == address(this), "Contract must own the NFT.");

startTime = block.timestamp;
endTime = startTime + (startPrice - endPrice) / priceDecrement;
emit AuctionStarted(startTime, endTime);
}

function buyNFT() external payable auctionActive {
uint256 currentPrice = getCurrentPrice();
require(msg.value >= currentPrice, "Ether sent is not enough.");

auctionEnded = true;
uint256 refund = msg.value - currentPrice;
if (refund > 0) {
payable(msg.sender).transfer(refund);
}
emit NFTPurchased(msg.sender, currentPrice);

nft.transferFrom(address(this), msg.sender, nftId);
}

function endAuction() public onlySeller {
require(!auctionEnded, "Auction already ended.");
require(block.timestamp >= endTime || nft.ownerOf(nftId) != address(this), "Auction cannot be ended yet.");

auctionEnded = true;
emit AuctionEnded(getCurrentPrice());
}

function getCurrentPrice() public view returns (uint256) {
if (block.timestamp >= endTime) {
return endPrice;
} else {
uint256 timeElapsed = block.timestamp - startTime;
uint256 priceReduction = priceDecrement * timeElapsed;
return startPrice - priceReduction;
}
}

function withdrawFunds() external onlySeller {
require(auctionEnded, "Auction not ended.");
uint256 funds = address(this).balance;
require(funds > 0, "No funds to withdraw.");

emit FundsWithdrawn(seller, funds);
payable(seller).transfer(funds);
}

function withdrawNFT() external onlySeller {
require(auctionEnded, "Auction not ended.");
require(nft.ownerOf(nftId) == address(this), "Contract does not own the NFT.");

emit NFTWithdrawn(seller, nftId);
nft.transferFrom(address(this), seller, nftId);
}
}

在这个合约中,卖家可以通过调用startAuction函数开始拍卖,买家可以通过发送足够的以太币并调用buyNFT函数来购买 NFT。如果拍卖没有人购买而结束,卖家可以通过调用endAuction来正式结束拍卖。卖家还可以在拍卖结束后通过withdrawFunds取出拍卖所得的以太币,通过withdrawNFT取回未售出的 NFT。

请注意,实际部署和使用此类合约时,您应该进行彻底的测试,并考虑所有可能的安全问题。此外,根据实际情况,您可能需要添加其他功能,例如暂停拍卖、紧急撤回 NFT 和资金的能力等。

NFT 调用

在这个简化的荷兰式拍卖合约中,买家通过调用 buyNFT 函数来出价并购买 NFT。当买家调用这个函数时,他们需要发送足够的以太币(至少等于当前的拍卖价格),这个价格是随着时间递减的。如果发送的以太币超过了当前价格,合约将会退还超出的部分。

这里是 buyNFT 函数的代码:

function buyNFT() external payable auctionActive {
uint256 currentPrice = getCurrentPrice();
require(msg.value >= currentPrice, "Ether sent is not enough.");

auctionEnded = true;
uint256 refund = msg.value - currentPrice;
if (refund > 0) {
payable(msg.sender).transfer(refund);
}
emit NFTPurchased(msg.sender, currentPrice);

nft.transferFrom(address(this), msg.sender, nftId);
}

这个函数首先检查拍卖是否处于活跃状态。然后,它计算当前价格并确认发送的以太币是否足够。如果一切正常,合约会标记拍卖已结束,将 NFT 从合约转移到买家,并退还任何超出当前价格的资金。

请注意,调用这个函数的时候,买家需要确保他们发送的以太币至少等于 getCurrentPrice 函数返回的当前价格。这不是一个传统意义上的出价,其中买家可以出一个低于当前价格的金额;而是一个立即购买的价格,买家必须支付这个价格或更高才能立即购买 NFT。

前端代码

在前端应用程序中调用智能合约函数通常涉及以下步骤:

  1. 准备环境:确保你有一个与以太坊网络交互的客户端库,如 web3.js 或 ethers.js。

  2. 设置合约实例:使用合约的 ABI(Application Binary Interface)和地址来创建一个合约实例。

  3. 与用户钱包交互:通过 MetaMask 或其他以太坊钱包与用户交互,请求他们的账户地址,并为交易签名。

  4. 调用合约函数:使用合约实例调用函数,并处理任何交易或返回值。

以下是如何使用 web3.js 或 ethers.js 库在前端调用buyNFT函数的示例:

使用 web3.js

// 引入web3.js库
const Web3 = require('web3');

// 连接到以太坊节点,这里假设你的用户使用的是MetaMask
const web3 = new Web3(window.ethereum);

// 请求用户账户
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const account = accounts[0]; // 获取用户的第一个账户

// 合约的ABI和地址
const contractABI = [
/* ...合约的ABI... */
];
const contractAddress = '0x...'; // 合约的地址

// 创建合约实例
const contract = new web3.eth.Contract(contractABI, contractAddress);

// 获取当前价格
const currentPrice = await contract.methods.getCurrentPrice().call();

// 调用buyNFT函数
contract.methods
.buyNFT()
.send({
from: account,
value: currentPrice,
})
.then(function (receipt) {
// 交易成功后的回调函数
console.log(receipt);
})
.catch(function (error) {
// 错误处理
console.error(error);
});

使用 ethers.js

// 引入ethers.js库
import { ethers } from 'ethers';

// 连接到以太坊节点,这里假设你的用户使用的是MetaMask
const provider = new ethers.providers.Web3Provider(window.ethereum);

// 请求用户账户
const signer = provider.getSigner();
const account = await signer.getAddress();

// 合约的ABI和地址
const contractABI = [
/* ...合约的ABI... */
];
const contractAddress = '0x...'; // 合约的地址

// 创建合约实例
const contract = new ethers.Contract(contractAddress, contractABI, signer);

// 获取当前价格
const currentPrice = await contract.getCurrentPrice();

// 调用buyNFT函数
const transaction = await contract.buyNFT({ value: currentPrice });

// 等待交易被挖掘
const receipt = await transaction.wait();

// 处理收据
console.log(receipt);

确保你的前端应用程序包括正确的智能合约 ABI 和地址,这些信息可以在合约部署后从开发工具(如 Truffle 或 Hardhat)获取,或者如果合约已经部署到公共网络,可以从区块链浏览器(如 Etherscan)获取。

此外,考虑到用户体验,你可能还需要在用户尝试购买 NFT 之前,提示他们当前的价格,并在交易发送到网络之后显示一些指示,比如一个加载指示器,直到交易完成。记得也要处理可能发生的错误,例如用户拒绝交易或者交易因为其他原因失败。

payable(msg.sender).transfer(refund) 是什么意思,多余的 eth 返回吗??

是的,payable(msg.sender).transfer(refund) 这行代码的目的是将多余的以太币(ETH)退还给函数调用者,也就是买家。

buyNFT 函数中,买家发送的以太币可能会超过当前的拍卖价格。这种情况通常发生在买家为了确保交易成功而发送额外以太币,或者是因为在他们发送交易和交易被矿工打包处理的时间内,拍卖价格已经下降。

这里是代码中处理退款的部分:

uint256 refund = msg.value - currentPrice;
if (refund > 0) {
payable(msg.sender).transfer(refund);
}

首先,计算出应该退还的金额 refund,这是通过从发送的以太币总额 msg.value 中减去当前价格 currentPrice 来得出的。如果这个计算结果大于零,说明买家发送了多余的以太币。

然后,使用 transfer 方法将这个多余的金额退还给买家。在 Solidity 中,transfer 方法是一种将以太币发送到指定地址的方法,而 payable 关键字用于指定一个地址可以接收以太币。因此,payable(msg.sender).transfer(refund) 将多余的以太币发送回调用这个函数的地址,也就是买家的地址。

需要注意的是,transfer 方法在发送失败时会抛出异常,导致整个交易被回滚。这意味着如果退款失败,整个购买 NFT 的操作也会失败。这通常被视为一种安全的方式来处理资金的发送,因为它确保了要么整个操作成功,要么完全不发生变化。然而,自从 Solidity 0.8.x 版本开始,由于它的内置重入攻击保护,使用 transfer 方法已经不再是唯一推荐的方式来发送以太币了,一些开发者可能会选择 call 方法来代替,因为它提供了更大的灵活性和更低的气费成本。