Cover photo

从代码层面聊聊 PartyDao

今天来聊聊一个神奇的项目 PartyDAO,这个项目最近融资1640万美元,估值两亿美金,其来源于推特的一番对话,具体可以参考超哥的一篇文章:PartyDAO的故事,对话的后续是 Mirror 的创始人 Denis Nazarov 在推特上提出了一个想法并讲了基本的流程,我把整个逻辑翻译下如下: 如果部署自动一个 DAO 来竞标任何 NFT 拍卖,那不是很酷吗?给它起名 PartyBid,它包含以下功能:

  • 构造函数:auction_id

  • 任何人存入 ETH,取回 ERC20

  • 合同自动投标拍卖

  • 如果获胜,资助者将获得 NFT 的部分所有权

  • 如果输了,资金退回

这个想法被一个程序员大佬 _anishagnihotri 看到了,于是他就在一个周末把这个功能实现了,在这里膜拜大佬,我们这里就看看整个代码是怎样实现的,首先大佬先把 Denis Nazarov 提出的想法扩展了下,想法毕竟是想法不是完整的 PM 需求,日常开发如果 PM 提出了这么个不完整的需求肯定是会挨程序员的骂的,但这里 anishagnihotri 显然是对这个想法感兴趣的,所以就把需求完善了下并描述了下该合约是如何工作的:

  1. 首先部署一个 PartyBidRA.sol 合约,构造函数有四个参数 (_ReserveAuctionV3Address, _auctionID, _bidAmount, _exitTimeout),其中 _ReserveAuctionV3Address 我们随后再讲,_auctionID 对应的是某 NFT 的 TokenId,bidAmount 对应的是出价,这里应该是 ETH 计价,exitTimeout 过期时间。

  2. 接下来,个人可以调用 join() 方法成为 DAO 的成员,发送他们的 ETH 来积累后续投标拍卖的资金。

于是后续会产生三种可能性:

  1. 这个 DAO 在 exitTimeout 时间之前参与人过少,筹集的 ETH 没有达到预订的 bidAmount, 此前参与的用户可以调用合约的 exit() 方法来提取他们投资的 ETH。

  2. 如果达到了预订的 bidAmount,DAO 成员可以调用 placeBid() 进行出价。如果 DAO 未能成功赢得 NFT,DAO 成员可以调用 exit() 来提取他们的资金。

  3. 筹集的资金达到了预订的 bidAmount,并且最终出价成功买入了该 NFT,后续会有如下的可能操作:

    1. DAO 成员可以调用 DAOProposeZoraBid() 方法发起一个新的提案:在 Zora 市场将该 NFT 进行出价卖出。

    2. DAO 成员可以调用 DAOVoteForZoraBidProposal() 对上述已提出的提案进行投票,提案需要超过 50% 的选票才能被执行。

    3. DAO 成员可以调用 DAOExecuteZoraBid() 来执行一个提案,该提案:(1) 拥有超过 50% 的 DAO 投票权,并且 (2) 自投票发起以来没有改变(以防止不良行为者在投票期间改变他们的出价) 投票过程)。 这会将 NFT 转移给出价者,并接受他们的资金到合约中。

    4. 最后,一旦成功转售,DAO 成员可以调用 exit() 来收回他们在投标金额中的份额。

至此,整个功能的描述就完成了,当然代码也是如此完成的。上述整个流程对于不在这个圈子里面的人来说有点难以理解,这里我举个例子去描述下: 小明想买一套房子,该房子房主目前出价 100 万,小明买不起,于是发起了一个众筹,一伙人看到了也觉得这个房子100万价格合适,于是纷纷加入,最终10个人加入了,每人出资10万元,成功将该房子买入了,但这个区域是学区房,房价过了一段时间还在涨,一直涨到了150左右,其中小张坐不住了,说咱们把房子卖出去吧,于是就发起了一个提案说咱们以 150 万的价格卖出,众人觉得可以,纷纷投票赞同,投票数超过5个人的时候,小张就执行了该提议,把房子卖给了出价 150万 的那个人,于是最后每个人欢欢喜喜的拿走了自己的15万。

整个流程其实和上述描述的大差不差,唯一和现实不同的是整个操作流程是在链上完成的,都是公开透明的,资金也都是按照个人初始投资进行均分的。

OK,基本流程已经说完了,我们看下代码:

首先说下 _ReserveAuctionV3Address 这个是拍卖合约的地址,placeBid() 方法其实针对的是某一个拍卖发起的报价,所以这个流程执行的过程依赖拍卖方的,创建拍卖的方法如下:

 function createAuction(
        uint256 tokenId,
        uint256 duration,
        uint256 reservePrice,
        uint8 curatorFeePercent,
        address curator,
        address payable fundsRecipient
    ) external nonReentrant whenNotPaused auctionNonExistant(tokenId) {
        // Check basic input requirements are reasonable.
        require(curator != address(0));
        require(fundsRecipient != address(0));
        require(curatorFeePercent < 100, "Curator fee should be < 100");
        // 初始化拍卖结构体信息
        auctions[tokenId] = Auction({
            duration: duration,
            reservePrice: reservePrice,
            curatorFeePercent: curatorFeePercent,
            curator: curator,
            fundsRecipient: fundsRecipient,
            amount: 0,
            firstBidTime: 0,
            bidder: address(0)
        });
        // Transfer the NFT into this auction contract, from whoever owns it.
        // 将要拍卖的 NFT 转移到拍卖合约
        IERC721(nftContract).transferFrom(
            IERC721(nftContract).ownerOf(tokenId),
            address(this),
            tokenId
        );
    }

当一个拍卖发起后,才可以执行 placeBid 进行叫价,最终到达拍卖结束时间后叫价最高的账户将赢得拍卖,结束拍卖的方法如下:

   function endAuction(uint256 tokenId)
        external
        nonReentrant
        whenNotPaused
        auctionComplete(tokenId)
    {
        // Store relevant auction data in memory for the life of this function.
        address winner = auctions[tokenId].bidder;
        uint256 amount = auctions[tokenId].amount;
        address curator = auctions[tokenId].curator;
        uint8 curatorFeePercent = auctions[tokenId].curatorFeePercent;
        address payable fundsRecipient = auctions[tokenId].fundsRecipient;
        // Remove all auction data for this token from storage.
        delete auctions[tokenId];
        // We don't use safeTransferFrom, to prevent reverts at this point,
        // which would break the auction.
        // 赢的拍卖的获取到该 NFT
        IERC721(nftContract).transferFrom(address(this), winner, tokenId);
        // First handle the curator's fee.
        if (curatorFeePercent > 0) {
            // Determine the curator amount, which is some percent of the total.
            uint256 curatorAmount = amount.mul(curatorFeePercent).div(100);
            // Send it to the curator.
            transferETHOrWETH(curator, curatorAmount);
            // Subtract the curator amount from the total funds available
            // to send to the funds recipient and original NFT creator.
            amount = amount.sub(curatorAmount);
            // Emit the details of the transfer as an event.
            emit CuratorFeePercentTransfer(tokenId, curator, curatorAmount);
        }
        // Get the address of the original creator, so that we can split shares
        // if appropriate.
        address payable nftCreator =
            payable(
                address(IMediaModified(nftContract).tokenCreators(tokenId))
            );
        // If the creator and the recipient of the funds are the same
        // (and we expect this to be common), we can just do one transaction.
        if (nftCreator == fundsRecipient) {
            transferETHOrWETH(nftCreator, amount);
        } else {
            // Otherwise, we should determine the percent that goes to the creator.
            // Collect share data from Zora.
            uint256 creatorAmount =
                // Call the splitShare function on the market contract, which
                // takes in a Decimal and an amount.
                IMarket(IMediaModified(nftContract).marketContract())
                    .splitShare(
                    // Fetch the decimal from the BidShares data on the market.
                    IMarket(IMediaModified(nftContract).marketContract())
                        .bidSharesForToken(tokenId)
                        .creator,
                    // Specify the amount.
                    amount
                );
            // 分红给 nftCreator
            transferETHOrWETH(nftCreator, creatorAmount);
            // 转移剩下的资金给拍卖方
            transferETHOrWETH(fundsRecipient, amount.sub(creatorAmount));
        }
    }

最核心的代码就是上面两部分了,完整的代码可以这里查看partybid初始代码。但你会发现和官网的操作并不一样,因为这份代码是最原始的版本,随着项目的进展,他们后续引入了 PM 以及有合约开发自荐进行迭代开发,新合约代码地址。所以你会发现,现在 partybid官网 的操作是没有拍卖的,因为后续是接入了类似 Opensea 市场直接进行买入,简化了拍卖的这一步,侧重点放到了众筹买入这个功能,当然有时间也可以积极的去官网进行参与下,说不定后面有空投惊喜呢。