开源艺术吞噬者智能合约

2022-09-20 |弗兰基变速器11戴夫怀特贾斯汀罗伊兰

内容

介绍

今天,我们很高兴地宣布,艺术吞噬者智能合约是开源的,可以在我们的Github存储库中使用。我们希望我们构建的系统能够吸引您的想象力,我们迫不及待地想看看您将在它们之上构建什么。

深入了解合同

在这篇文章中,我们想重点介绍Art Gobblers代码库中一些有趣的部分,比如我们的自定义ERC721实现,GOO集成,我们的渐进式显示系统以及我们的测试方法。

自定义 ERC721 实现

吞噬者ERC721

艺术吞噬者比普通的NFT项目复杂得多。吞噬者使用VRGDA出售,并使用自定义实用程序令牌(Goo)支付。最重要的是,每个Gobbler都被分配了一个“发射倍数”,这决定了它使用GOO发行连续生成Goo代币的速率。

尽管如此,我们希望铸造和转移吞噬者NFT的天然气成本是业内最低的。

为了实现这一目标,我们构建了一个定制的ERC721实现,该实现大量使用结构包装以保持效率。我们能够在通常用于所有者和余额信息的映射中拟合与每个吞噬者和用户相关的所有状态。

对于吞噬者状态,我们将每个吞噬者的 160 位所有者地址与 2 个其他变量打包在一起:和:idxemissionMultiple

/// @notice Struct holding gobbler data.
struct GobblerData {
    // The current owner of the gobbler.
    address owner;
    // Index of token after shuffle.
    uint64 idx;
    // Multiple on goo issuance.
    uint32 emissionMultiple;
}

对于所有者状态,我们将该用户拥有的吞噬者的 32 位计数与 3 个与 GOO 颁发相关的其他变量打包在一起:、和:emissionMultiplelastBalancelastTimestamp

struct UserData {
  // The total number of gobblers currently owned by the user.
  uint32 gobblersOwned;
  // The sum of the multiples of all gobblers the user holds.
  uint32 emissionMultiple;
  // User's goo balance at time of last checkpointing.
  uint128 lastBalance;
  // Timestamp of the last  goo balance checkpoint.
  uint64 lastTimestamp;
}

虽然在引擎盖下,所有这些数据都打包在一个256位插槽中,但GobblersERC721仍然100%符合ERC721接口,这要归功于帮助程序功能,它公开了所需的特定存储部分,ownerOfbalanceOf

function ownerOf(uint256 id) external view returns (address owner) {
  require((owner = getGobblerData[id].owner) != address(0), "NOT_MINTED");
}

function balanceOf(address owner) external view returns (uint256) {
  require(owner != address(0), "ZERO_ADDRESS");

  return getUserData[owner].gobblersOwned;
}

此外,GobblersERC721 还具有优化功能,可利用艺术吞噬者系统的基本不变量(不会铸造任何令牌、的供应上限等)来删除多余的断言。address(0)10,000

页面显示721

页面虽然比吞噬者复杂得多,但也使用改进的ERC721实现来改善气体成本和将页面提供给吞噬者的用户体验。

首先,Pages 文稿在被 Gobblers 合约转让时会自动跳过审批检查,使吞噬过程变得简单,而不会遇到额外的麻烦。

function isApprovedForAll(address owner, address operator) public view returns (bool isApproved) {
  if (operator == address(artGobblers)) return true; // Skip approvals for the ArtGobblers contract.

  return _isApprovedForAll[owner][operator];
}

而且,像吞噬者一样,Pages利用艺术吞噬者系统的已知不变量来删除其铸造逻辑中的多余断言。

集成咕咕

吞噬者以指定的排放率产生Goo,我们在GOO论文中对此进行了讨论。由于 Goo 是随时间连续生成的,因此需要延迟计算 Goo 余额。也就是说,跟踪虚拟余额,可以在用户方便的时候将其兑换成常规的ERC20余额。

由于这种虚拟平衡是用户总发射倍数的函数,因此必须确保在发生传输时保持正确。例如,当用户将吞噬者转移出去时,他们的总排放量倍数应该会下降(他们未来的排放量也应该下降),但他们当前的粘量平衡不应该改变。

为了解决这个问题,我们覆盖了Gobbler的传递函数,以确保拍摄用户状态的正确快照。

function transferFrom(
  address from,
  address to,
  uint256 id
) public override {
  ... snip ...

  unchecked {
    // Caching saves gas.
    uint32 emissionMultiple = getGobblerData[id].emissionMultiple; 

    // Update the sending address's user data.
    getUserData[from].lastBalance = uint128(gooBalance(from));
    getUserData[from].lastTimestamp = uint64(block.timestamp);
    getUserData[from].emissionMultiple -= emissionMultiple;
    getUserData[from].gobblersOwned -= 1;

    // Update the receiving address's user data.
    getUserData[to].lastBalance = uint128(gooBalance(to));
    getUserData[to].lastTimestamp = uint64(block.timestamp);
    getUserData[to].emissionMultiple += emissionMultiple;
    getUserData[to].gobblersOwned += 1;
  }

  emit Transfer(from, to, id);
}

由于虚拟余额用于跟踪Goo,我们希望避免购买成为多交易过程。通常,用户必须提交一笔交易才能将他们的虚拟余额转换为常规ERC20余额,一笔交易将Gobblers批准为Goo消费者,另一笔交易才能购买物品。

为了简化这一过程,我们修改了 Gobblers 和 Pages 中的购买功能,以便用户能够直接从虚拟余额中支出,并在合同之间设置权限,以便无需额外的审批交易。

渐进式揭示

艺术吞噬者薄荷和揭示过程有一些独特的限制。我们的目标是使该过程在保持气体效率的同时具有可证明的公平性。但是,由于Gobblers将在10年内被铸造,因此等到造币厂结束才进行完整的收藏展示并不是一种选择。

为了实现这一点,我们使用费雪耶茨洗牌实现了高度优化的批量显示过程。这允许每天发生一次揭示,使用Chainlink作为我们的随机性提供者。

在揭示过程中,这种随机性用于将元数据分配给每个未公开的吞噬者。这包括令牌 ID 以及从预定义分布中采样的发射乘数。

function revealGobblers(uint256 numGobblers) external {
  uint256 randomSeed = gobblerRevealsData.randomSeed;

  ... snip ...

  unchecked {
    for (uint256 i = 0; i < numGobblers; ++i) {
        
    ... snip ...

    // Randomly pick distance for swap.
    uint256 distance = randomSeed % remainingIds;

    // Select swap id, adding distance to next reveal id.
    uint256 swapId = currentId + distance;

    // Get the index of the swap id.
    uint64 swapIndex = getGobblerData[swapId].idx == 0
      ? uint64(swapId) // Hasn't been shuffled before.
      : getGobblerData[swapId].idx; // Shuffled before.

    // Get the index of the current id.
    uint64 currentIndex = getGobblerData[currentId].idx == 0
      ? uint64(currentId) // Hasn't been shuffled before.
      : getGobblerData[currentId].idx; // Shuffled before.

    // Swap indices
    getGobblerData[currentId].idx = swapIndex;
    getGobblerData[swapId].idx = currentIndex;

    ... snip ... 
  }
}

由于一些巧妙的结构包装和各种额外的优化,该过程可以以气体高效的方式运行。

我们还使我们的随机性提供程序可升级(编写精简的提供程序接口适配器)。这是必要的,因为我们无法找到一家VRF提供商来保证在造币厂期间(约10年)提供服务。

测试与安全

我们做出了巨大的努力来确保这些智能合约的正确性,包括各种形式的测试和审计。

测试

我们编写了多个单元测试和模糊测试,利用 Foundry 作为测试框架。我们还大量使用差分模糊测试来测试一些数学上更复杂的行为。VRGDA和Goo的实现是用python编写的,并且这些实现已经针对Solidity进行了模糊处理,以确保输出是等效的:

function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public {
  ... snip ...

  // Calculate actual price from VRGDA.
  actualPrice = gobblers.getVRGDAPrice(toDaysWadUnsafe(timeSinceStart), numSold);

  // Calculate expected price from python script.
  uint256 expectedPrice = calculatePrice(timeSinceStart, numSold);

  // Equal within 1 percent.
  assertRelApproxEq(actualPrice, expectedPrice, 0.01e18);
}

function calculatePrice(
  uint256 timeSinceStart,
  uint256 numSold
) private returns (uint256) {
  string[] memory inputs = new string[](7);
  inputs[0] = "python3";
  inputs[1] = "analysis/python/compute_price.py";
  inputs[2] = "gobblers";
  inputs[3] = "--time_since_start";
  inputs[4] = timeSinceStart.toString();
  inputs[5] = "--num_sold";
  inputs[6] = numSold.toString();

  return abi.decode(vm.ffi(inputs), (uint256));
}

我们还利用了先进的安全工具,例如用于静态分析的Slither和用于自动定理证明的Z3,以验证我们对该机制的一些假设。

激励游戏测试

对我们来说,验证吞噬者游戏玩法是否平衡也很重要。我们想测试是否有任何策略可以让玩家最终获得不成比例的游戏资源(无论是吞噬者,页面还是Goo)。

In order to do this, we ran an incentivized play test, organized with some help from Grug. We tuned the mechanism’s parameters to speed up gameplay by 30x, and invited a group of searchers and developers to play. We tasked them with trying to obtain as many Gobblers and as much Goo as they could, with Gobblers being given as prizes.

 

Final standings of our incentivized play test
Final standings of our incentivized play test

 

This was an interesting experiment, and we were able to observe some fun strategies emerge. We encourage developers building onchain games to include play testing as part of their development process.

Audits and C4 Contest

For audits, we underwent an internal review process by samczsun and Riley Holterhus. We also engaged Spearbit to conduct an audit of the protocol near the completion of its current development, which did not uncover any major vulnerabilities.

我们也很高兴地宣布,今天,为期一周的审计竞赛将以code4rena启动,奖金为100,000美元。我们邀请大家参加,看看你们能找到什么。

结论

我们很高兴与您分享艺术吞噬者合同,并期待看到您将构建的内容。

如果您有任何项目,我们很乐意听取您的意见。你可以在推特上@FrankieIsLost,@transmissions11@_Dave__White_@JustinRoiland与我们联系。