由于以太猫合约遵循的是ERC721标准的合约协议,所以每只猫的价值都不一样。在以太猫的游戏中,每只猫的价值是通过拍卖来体现的。在日常生活中,拍卖都是从从低价到最高价拍卖的,而在以太猫的游戏 合约中,拍卖限定了最高价、最低价和拍卖时间,竞拍者是从高价到低价进行竞拍的,这样节约了由于大量低价竞拍最后没有成交造成gas浪费和网络堵塞。接下来就重点来分析相关代码。
拍卖合约(基础接口)
// 拍卖合约,包含了模型、变量、以及拍卖的内部函数
contract ClockAuctionBase {
// 拍卖的结构体
struct Auction {
address seller; // 代币的拥有者
uint128 startingPrice; // 起始拍卖价格
uint128 endingPrice; // 限定最低的拍卖价格
uint64 duration; // 拍卖的存续期(拍卖时间,一天,一个月)
uint64 startedAt; // 拍卖开始时间,startedAt为0时,标识拍卖结束了
}
ERC721 public nonFungibleContract; // ERC721 接口类
uint256 public ownerCut; // 手续费百分比,0%-100%
mapping (uint256 => Auction) tokenIdToAuction; // ERC721代币对应的拍卖结构体
event AuctionCreated(uint256 tokenId, uint256 startingPrice, uint256 endingPrice, uint256 duration); // 拍卖创建事件
event AuctionSuccessful(uint256 tokenId, uint256 totalPrice, address winner); // 拍卖成功事件
event AuctionCancelled(uint256 tokenId); // 拍卖移除事件
// 判断代币的拥有者是否为_claimant
function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
return (nonFungibleContract.ownerOf(_tokenId) == _claimant);
}
function _escrow(address _owner, uint256 _tokenId) internal {
// 相当于先把我的代币转移给了本合约,由本合约先管理我的代币(托管)
nonFungibleContract.transferFrom(_owner, this, _tokenId);
}
// 通过调用ERC721接口中的transfer方法,实现了代币的转移
function _transfer(address _receiver, uint256 _tokenId) internal {
nonFungibleContract.transfer(_receiver, _tokenId);
}
// 增加了一个新的拍卖
function _addAuction(uint256 _tokenId, Auction _auction) internal {
// _tokenId 要拍卖的代币的id;_auction 拍卖的结构体
require(_auction.duration >= 1 minutes); // 拍卖的时间必须要大于1分钟
tokenIdToAuction[_tokenId] = _auction; // 绑定 代币ID 和 拍卖
AuctionCreated( // 触发一个拍卖创建事件
uint256(_tokenId),
uint256(_auction.startingPrice),
uint256(_auction.endingPrice),
uint256(_auction.duration)
);
}
// 退出拍卖
function _cancelAuction(uint256 _tokenId, address _seller) internal {
_removeAuction(_tokenId); // 移除拍卖
_transfer(_seller, _tokenId); // 将代币返回给拍卖者,之前是托管在合约
AuctionCancelled(_tokenId); // 触发移除拍卖事件
}
// 拍卖完成,出_bidAmount金额购买_tokenId代币
function _bid(uint256 _tokenId, uint256 _bidAmount)
internal
returns (uint256)
{
Auction storage auction = tokenIdToAuction[_tokenId]; // 获取拍卖引用,通过代币_tokenId
require(_isOnAuction(auction)); // 判断是否还在拍卖,如果为0 ,说明拍卖结束了
uint256 price = _currentPrice(auction); // 获取当前拍卖价格,拍卖价格是动态的,是随着时间的变化而减少的
require(_bidAmount >= price); // 出价 > 当前的拍卖价格
address seller = auction.seller; // 获取拍卖的卖家
_removeAuction(_tokenId); // 移除拍卖
if (price > 0) { // 当前拍卖价格 要 大于 0
uint256 auctioneerCut = _computeCut(price); // 计算手续费
uint256 sellerProceeds = price - auctioneerCut; // 卖家可以获得的金额 = 最终价格 - 手续费
seller.transfer(sellerProceeds); // 转账到卖家的账户中
}
uint256 bidExcess = _bidAmount - price; // 计算多余的金额
msg.sender.transfer(bidExcess); // 返还多余的
AuctionSuccessful(_tokenId, price, msg.sender); // 触发拍卖成功事件
return price;
}
// 从mapping中移除拍卖
function _removeAuction(uint256 _tokenId) internal {
delete tokenIdToAuction[_tokenId];
}
// 判断是否还在拍卖,如果startedAt 起始事件 为 0 ,说明已经拍卖结束
function _isOnAuction(Auction storage _auction) internal view returns (bool) {
return (_auction.startedAt > 0);
}
// 获取当前拍卖价格
function _currentPrice(Auction storage _auction)
internal
view
returns (uint256)
{
uint256 secondsPassed = 0;
if (now > _auction.startedAt) { // 当前时间要大于拍卖开始时间
secondsPassed = now - _auction.startedAt; //计算拍卖开始多少时间了
}
return _computeCurrentPrice(
_auction.startingPrice,
_auction.endingPrice,
_auction.duration,
secondsPassed
);
}
// 计算当前的拍卖价格
// 拍卖的价格是随着时间而逐渐降低的,如果没有人购买的话
function _computeCurrentPrice(
uint256 _startingPrice,
uint256 _endingPrice,
uint256 _duration,
uint256 _secondsPassed
)
internal
pure
returns (uint256)
{
// 拍卖已经开始了多少时间 > 拍卖存续期 ,说明拍卖已经结束,返回最终价格
if (_secondsPassed >= _duration) {
return _endingPrice;
} else {
// 当前的价格,totalPriceChange 实际上是负数,因为_endingPrice 永远小于 _startingPrice
int256 totalPriceChange = int256(_endingPrice) - int256(_startingPrice);
// 随着时间的流逝,currentPriceChange(负数) 会越来越小,
// 最小 = 拍卖限定最低价 - 拍卖起始价格
int256 currentPriceChange = totalPriceChange * int256(_secondsPassed) / int256(_duration);
// 当前价格 = 起始价格 + currentPriceChange(负数)
// 当前价格最低等于 限定最低价
int256 currentPrice = int256(_startingPrice) + currentPriceChange;
return uint256(currentPrice);
}
}
// 计算手续费
function _computeCut(uint256 _price) internal view returns (uint256) {
// ownerCut 为 0 到 10000的数,代表的是万分只几的手续费率
return _price * ownerCut / 10000;
}
}
从上述的合约上可以看到,随着拍卖时间的推移,程序限定的当前价格会逐渐减少。再次期间,只要竞拍者的出价大于当前价格即可成交。由于合约部署在主链上,而主链上的数据是透明的,我们可以通过抓取一个发起拍卖的交易,从交易数据中获取拍卖的最高价和最低价,以及拍卖起始时间,我们就可以通过代码中的算法换算出当前的竞拍价格。好在开发人员意识到这一点,当目前的竞拍价大于定价时,会将多余的钱给竞拍者退回账户。平台的盈利就是获取拍卖的手续费。
拍卖合约
contract ClockAuction is Pausable, ClockAuctionBase {
// 接口验证,判断是否ERC721协议
bytes4 constant InterfaceSignature_ERC721 = bytes4(0x9a20483d);
// 构造函数
function ClockAuction(address _nftAddress, uint256 _cut) public {
require(_cut <= 10000); // 交易费判断
ownerCut = _cut;
ERC721 candidateContract = ERC721(_nftAddress); // 实例化 ERC721 接口,_nftAddress为ERC721协议接口实例的合约地址
require(candidateContract.supportsInterface(InterfaceSignature_ERC721)); // 代币是否满足规定的要求,拥有我想要的函数
nonFungibleContract = candidateContract; // 赋值给ClockAuctionBase中的nonFungibleContract
}
// 转账,将当前的拍卖合约的以太币转移给ERC721合约
function withdrawBalance() external {
address nftAddress = address(nonFungibleContract); // 把接口强转为地址类型
require( // 权限判断
msg.sender == owner ||
msg.sender == nftAddress
);
// 进行转账
bool res = nftAddress.send(this.balance);
}
// 创建一个新的拍卖
function createAuction(
uint256 _tokenId, // 代币的id
uint256 _startingPrice, // 开始的价格
uint256 _endingPrice, // 最低的价格
uint256 _duration, // 拍卖的存取期
address _seller // 拍卖者
)
external
whenNotPaused
{
// 限制_startingPrice,_endingPrice,_duration 都不能超过所指定的范围
require(_startingPrice == uint256(uint128(_startingPrice)));
require(_endingPrice == uint256(uint128(_endingPrice)));
require(_duration == uint256(uint64(_duration)));
require(_owns(msg.sender, _tokenId)); // 合约的调用者 必须 是代币的拥有者
_escrow(msg.sender, _tokenId); // 托管(合约的调用者的)代币到合约中
Auction memory auction = Auction( // 构造拍卖的结构体
_seller,
uint128(_startingPrice),
uint128(_endingPrice),
uint64(_duration),
uint64(now)
);
_addAuction(_tokenId, auction); // 增加了一个新的拍卖
}
// 完成拍卖
function bid(uint256 _tokenId)
external
payable
whenNotPaused
{
_bid(_tokenId, msg.value); // 处理金额,调用父合约ClockAuctionBase 中的_bid
_transfer(msg.sender, _tokenId); // 把代币的所有权转移给买家
}
// 退出拍卖,外部函数
function cancelAuction(uint256 _tokenId) external
{
Auction storage auction = tokenIdToAuction[_tokenId]; // 拍卖的引用
require(_isOnAuction(auction)); // 判断是否在交易期
address seller = auction.seller; // 获取拍卖人的地址
require(msg.sender == seller); // 判断合约的调用者必须是拍卖人
_cancelAuction(_tokenId, seller); // 退出拍卖
}
// 游戏暂停时调用,只有拍卖的拥有者才能调用(防止程序遇到bug时,可以补救)
function cancelAuctionWhenPaused(uint256 _tokenId) whenPaused onlyOwner external
{
Auction storage auction = tokenIdToAuction[_tokenId]; // 拍卖的引用
require(_isOnAuction(auction)); // 判断是否在交易期
_cancelAuction(_tokenId, auction.seller); // 退出拍卖
}
// 获取拍卖信息,外部,不花费gas的调用,可以再前端查询
function getAuction(uint256 _tokenId) external view
returns
(
address seller,
uint256 startingPrice,
uint256 endingPrice,
uint256 duration,
uint256 startedAt
) {
Auction storage auction = tokenIdToAuction[_tokenId]; // 拍卖的引用
require(_isOnAuction(auction)); // 判断是否在交易期
return (
auction.seller,
auction.startingPrice,
auction.endingPrice,
auction.duration,
auction.startedAt
);
}
// 获取代币的当前价格
function getCurrentPrice(uint256 _tokenId)
external
view
returns (uint256)
{
Auction storage auction = tokenIdToAuction[_tokenId]; // 拍卖的引用
require(_isOnAuction(auction)); // 游戏必须在运行
return _currentPrice(auction);
}
}
ClockAuction 合约继承了ClockAuctionBase 合约。其实ClockAuctionBase 是低层具体功能的实现;ClockAuction 是基于ClockAuctionBase 的二次封装。
游戏启停控制合约
contract Pausable is Ownable {
// 事件
event Pause();
event Unpause();
bool public paused = false;
// 函数修饰器,没有暂停
modifier whenNotPaused() {
require(!paused);
_;
}
// 函数修饰器,暂停
modifier whenPaused {
require(paused);
_;
}
function pause() onlyOwner whenNotPaused returns (bool) {
paused = true;
Pause();
return true;
}
function unpause() onlyOwner whenPaused returns (bool) {
paused = false;
Unpause();
return true;
}
}