0xSplits 是一个收入拆分协议,为低 Gas 实现收入拆分提供了很好的实现思路。
- 原文链接: https://www.solidnoob.com/blog/0xSplits 作者: @Nazar Ilamanov
- 译文出自:登链翻译计划
- 译者:翻译小组 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
译者导读:很多时候,我们需要把一份收入(金额)拆分到不同的地址,而又不想让付款人(通常是用户)承担拆分收入部分的 GAS费用, 0xSplits 的架构及解决思路值得我们参考。
本文先介绍了0xSplits 是什么,0xSplits 采用的架构是怎么样的,同时介绍了几种其他的拆分方式可能带来的问题,另外还有一些0xSplits在 Gas 优化上的做法。
0xSplits 是链上收入拆分的协议, 它是一个智能合约,允许你将任何付款拆分到许多收款人,例如,它可以用于NFT版税。EIP-2918允许你只指定一个地址作为收款人,但如果你把这个地址设置为0xSplits地址,你就可以有很多个收款人。
0xSplits由一个主合约组成,可以创建许多子克隆合约(SplitWallet
)。
你通过调用主合约创建一个新的拆分(split)
它创建一个负责接收ETH的子合约
在需要接收ETH的地方使用子合约的地址
译者注:克隆合约是使用同一份代码,创建(复制)多个出合约, 每个合约都有自己的状态。
这里是子克隆合约的代码,你可以看到,它非常简单。它可以接收ETH,并且有一个函数可以将ETH转移到主合约, 0xSplits也支持ERC-20,但省略了这部分代码:
但收到的ETH如何拆分给接收者?这就是主合约的任务(为了便于阅读,代码做了修改):
所以,资金是静止不动的,直到有人明确地调用 distributeETH
。但为什么有人会浪费Gas来调用这个函数呢?答案是distributorFee
(分配费用)。这是在创建拆分时设置的,谁调用distributionETH
就归谁。
这种激励性的代码调用是必要的,因为EVM没有周期任务(cron )或回调来定期分配资金,一切都需要由个人或链外机器人发起,distributorFee有点类似于CryptoKitties合约中的autoBirthFee
。
为什么0xSplits
会选择这种架构?还有,为什么不在收到资金的时候立即分发?这是我们这篇文章的主要想解决的问题。
你可以在Etherscan找到0xSplits的代码
克隆SplitWallet
合约接收付款并持有资金,直到它们被调用,将资金转移到主合约。
分销商(distributor
)被激励在主合约中调用distributeEth
,这将把资金从克隆合约转移到主合约中,同时也将支付/分成。
但是在这一点上,资金仍然存放在主合约中(合约有一个从收款人地址到余额的映射)。为了实际接收资金,用户必须调用主合约中的withdraw
函数。
现有的设定是相当复杂的:克隆合约接收付款,分销商(distributor
)将这些资金从克隆合约转移到主合约,并将资金分给接收者。但是收款人仍然需要调用withdraw
来接收实际拆分到的资金。所以,端到端的用户流程并不是完全自动化的(另外,有些钱会被分销商收取)。
为什么0xSplits需要如此复杂的设置?让我们来看看其他的方法,以及它们为什么不可行。
为什么不在收到资金的同一函数中进行拆分呢?也就是说,不在合约中保存资金, 而是立即分配,这里有2个原因:
transfer
)到克隆合约中。而是要求付款人将不得不调用合约上的一个特定函数(来进行拆分),这会破坏互操作性。你可能会说只要重写receive
函数,但receive
函数经常受2300个Gas限制,这意味着你不能在其函数中进行大量的转账
。为什么没有一个共享的合约,为所有可能的拆分接收所有的付款?这将摆脱克隆(创建新合约),但它并不奏效,因为当共享合约收到付款时,它不知道付款是要进行怎样的拆分。
distributeEth
和withdraw
合二为一呢?为什么不把整个过程端到端自动化,让分销商在一个函数中执行这2个操作?主要是因为安全问题。强烈建议使用pull over push - 即让用户手动拉取他们的自己资金,而不是把资金自动推送他们。
为什么 "推(push)"被认为是一种安全风险?阅读下面的链接以获得完整的答案,但简而言之,这是因为不是所有的接收者都能保证正确处理ETH的接收。一些恶意的行为者可能会部署一个智能合约,在 receive
功能中进行回退(revert)。因此,如果至少有一个接收者回退(revert),整个操作就会回退(revert)。
参考阅读文章:Pull over Push 将转移以太坊的相关风险转移到用户身上。
如果你想出另一种替代架构,请告诉我(在评论中这里)。
我认为0xSplits
为他们所针对的使用场景选择了正确的架构--这就是成为DeFi生态系统中的一个基本构件。还有其他类似的支付拆分器,它们是针对不同的使用场景,它们使用不同的架构。例如,disperse.app在收到资金时进行拆分,它可能是一个更好的一次性付款拆分工具。
我对合约进行了重组,并将所有内容按功能分组,以方便阅读。
现在我们来看看现有的架构是如何在代码中实现的。
这里只有2个合约:SplitMain.sol
和SplitWallet.sol
(克隆)。其余的是库和接口。
你已经在前面看到了SplitWallet.sol
的代码,但我把再次复制到这里只是为了参考。
这很简单。它可以接收ETH并将资金转账到主合约。你可能会问,如果没有 receive
功能,它如何接收ETH?答案是,"克隆 "库创建该合约的克隆,神奇地插入 receive
功能的汇编代码。
现在到了主合约 - SplitMain.sol
, 这是所有行动发生的地方。
SplitMain.sol
合约开头的函数,用于创建新的拆分。
validSplit
只是验证一些东西,如:
百分比的总和应该是1。
收款人和百分比数组具有相同的长度。
收款人地址数组需要被排序。为什么?我们很快就会知道。
如果controller
是零地址,这意味着拆分没有所有者,它变得不可改变。Clones
库将创建一个SplitWallet
合约的克隆,并保存在构造函数中。
clone
和cloneDeterministic
(在上面的createSplit
函数中)的区别是,确定性的部署到一个预先定义的地址(由传入的splitHash
决定)。不可变(Immutable)的拆分合约使用确定性的克隆,以避免有人创建完全相同的拆分合约时时发生碰撞。
拆分(split)是由这些数据结构表示的:
请注意,上面只保存了拆分的哈希值,而没有保存地址、收款人和distributorFee。为什么我们需要哈希值?
哈希值是用来将所有关于拆分的信息(收款人、百分比、distributorFee)合成一个字符串。
通过只存储哈希值而不是函数的所有参数,我们节省了大量的存储空间,从而节省了Gas。
但是,我们如何查找在哈希过程中丢失的信息,如recipients
?我们要求这些信息被传递到需要它们的函数中。在合约中,我们只需再次对传入的参数进行散列,并与存储的散列进行比较。如果它们匹配,传入的参数就是正确的。
分销商被激励在链外记录recipients
, percentages
等,并将这些信息传递给所有需要它们的函数。分销商费用则是支付他们的服务的费用。
另外,我们现在明白为什么收款人地址数组需要在createSplit
函数中进行排序。因为否则,哈希值将无法重现。
用哈希值更新拆分也变得非常有效。只要更新哈希值即可。
(onlySplitController
确保msg.sender == split.controller
)
如果一个拆分是可变的,你可以转移其所有权。
这是有两步过程:
为什么是两步程序?为了防止意外转移到一个错误的地址。两步程序使你的合约更安全一些(代价是略微多一点Gas)。
资金是如何分配的?让我们来看看:
我们首先通过哈希算法验证传入的args,并与存储的哈希算法进行比较。然后我们将资金转移到主合约,为分销商预留奖励,最后分配资金。
这个函数为了可读性做了大量的修改。请阅读原始的源代码以了解实际的实现。
主合约的最后一个功能是收款人提取资金的能力。这是一个非常简单的功能。
由于ethBalances
中的资金来于所有的克隆拆分合约。但是withdraw
需要手动调用,机器人/分销商没有受到激励进行这个动作。
同样有趣的是,有人可以代表你调用withdraw
(为你的Gas付钱)。
0xSplits 实际上允许你有嵌套的拆分--指定一个拆分为另一个拆分的收款人。
0xSplits 也适用于 ERC-20 资金的拆分。我只是省略了代码以方便阅读。
不小心发送到主合约的资金是无法恢复的,因为没有办法提取多余的ETH。
OpenZeppelin也有一个支付拆分器,但我还没有研究过它是如何实现的,还有disperse.app,对于一次性的拆分来说可能更好。
用比特币而不是用以太坊来拆分支付要容易得多。由于UTXO架构的存在,比特币几乎拥有这种开箱即用的功能。(见 Solidity Fridays)
就这样吧!让我知道你对0xSplits的看法,在评论区。
感谢 Chaintool 对本翻译的支持, Chaintool 是一个为区块链开发者准备的开源工具箱
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!