创建 ERC20 代币支付拆分智能合约
在加密货币的几乎每个领域,支付都是一个反复出现的话题,特别是向多个质押者提供支付。例如,DAO希望为多个计划提供资金,DEX希望合并向某些参与者分配交易费用,或者团队希望将代币作为月薪分发给团队成员。
智能合约使我们能够自动化这些类型的支付功能,这就限制了人工管理支付所导致的潜在错误,并允许我们将宝贵的时间花在其他生产性任务上。
今天,我们将学习如何创建自己的ERC20代币支付拆分器,它可以合并到任何项目中!
先决条件和设置
下面的内容要求你对Solidity有点熟悉,不过任何人都可以学习。
项目架构
我们将创建两个合约。第一个将是ERC20代币支付拆分智能合约,第二个将是模拟池智能合约。ERC20代币支付拆分器智能合约将是抽象的,并持有用于管理收付方及其各自支付部分的逻辑和数据。模拟池将继承ERC20代币支付拆分器,以便我们可以自动将支付分发给多个质押者。在两个合约中拆分支付功能的原因有两个:
展示在真实世界的用例中代币支付拆分合约的使用
确保代币支付拆分合约足够灵活,任何人都可以选择并集成到自己的项目中
OpenZeppelin已有一个名为PaymentSplitter.sol的智能合约。用于以太坊支付拆分。我们将利用这个现有的功能并对其进行定制,使其能够与ERC20代币一起工作。
设置开发环境
本教程中的工具:
安全帽——智能合约开发环境
OpenZeppelin -经过审计的智能合约模板
现在在一个空目录中使用NPM init -y启动一个NPM项目
设置项目后,使用以下命令安装 Hardhat:
在安装了Hardhat之后,输入npx Hardhat并选择创建基本示例项目的选项。这将包括一个方便的文件结构,可以轻松地创建、测试和部署您自己的合约。
选择创建基本示例项目
可以删除contract 文件夹中的Greeter.sol文件,并从test文件夹中删除sample-test.js文件。
我们还将安装安全帽插件库,它们是Hardhat插件。它们允许我们添加用于测试和部署智能合约的工具。
在hardhat.config.js文件的顶部,添加
需安装一个叫chai的包,用来测试我们的智能合约。
需安装OpenZeppelin合约库。
创建代币支付拆分器
这个代币支付拆分智能合约将提供逻辑来设置和存储涉及收款人列表和每个收款人份额的数据。每个收款人持有的份额数等于他们应该获得的资金比例(例如,如果有4个收款人,每个人持有5份额,那么他们每个人将获得任何支出的25%)。
要开始这个合约,我们将在我们的合约文件夹中创建一个新文件,并将其命名为TokenPaymentSplitter.sol。
设置pragma line和contract shell。
pragma solidity ^0.8.0;abstract contract TokenPaymentSplitter {
}
注意,这是一个抽象合约,我们稍后将把它导入模拟池合约。使它成为抽象的,也允许我们在未来轻松地将这个合约导入到任何其他真实的项目中。
现在让我们从OpenZeppelin导入一个有用的工具。
pragma solidity ^0.8.0;import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";abstract contract TokenPaymentSplitter { using SafeERC20 for IERC20;
}
SafeERC20.sol 提供了ERC20接口,该接口允许我们从任何ERC20智能合约调用标准函数,并将这些调用包装在附加功能中,以提供更安全的方式传输代币。
现在,我们将创建变量来存储合约数据。
abstract contract TokenPaymentSplitter { using SafeERC20 for IERC20; address internal paymentToken; uint256 internal _totalShares; uint256 internal _totalTokenReleased; address[] internal _payees; mapping(address => uint256) internal _shares; mapping(address => uint256) internal _tokenReleased;
}
paymentToken是我们用于支付的ERC20代币的地址。
_totalShares提供来自所有收款人的份额相加。
_totalTokenReleased是已支付给所有收款人的支付代币总额。
_payees提供了当前所有收款人地址的数组。
_shares是收款人地址与分配给他们的份额数量的映射。
_tokenReleased是收款人地址到支付代币数量的映射。
现在放置一个接受三个参数的构造函数。第一个参数是我们希望在合约部署中初始化的收款人的数组。第二个参数是每个收款人的份额数组。第三个是将用于支付的ERC20代币的地址。
pragma solidity 0.8.0constructor( address[] memory payees, uint256[] memory shares_, address _paymentToken) { require( payees.length == shares_.length, "TokenPaymentSplitter: payees and shares length mismatch" ); require(payees.length > 0, "TokenPaymentSplitter: no payees"); for (uint256 i = 0; i < payees.length; i++) { _addPayee(payees[i], shares_[i]); } paymentToken = _paymentToken;
}
构造函数包含一个require语句,以确保两个数组具有相同的长度,以便每个收款人都有分配给他们的份额。还有另一个require语句,以确保合约初始化与至少有一个收款人。
还有一个for循环,它将每个收款人及其份额分配我们上面创建的变量。这是通过一个名为_addPayee的函数完成的,我们将很快创建这个函数。
构造函数就绪后,再添加几个函数来调用和获取合约变量。
pragma solidity 0.8.0function totalShares() public view returns (uint256) { return _totalShares;}function shares(address account) public view returns (uint256) { return _shares[account];}function payee(uint256 index) public view returns (address) { return _payees[index];
}
现在我们将创建用于添加收款人的函数。
pragma solidity 0.8.0;function _addPayee(address account, uint256 shares_) internal { require( account != address(0), "TokenPaymentSplitter: account is the zero address" ); require(shares_ > 0, "TokenPaymentSplitter: shares are 0"); require( _shares[account] == 0, "TokenPaymentSplitter: account already has shares" ); _payees.push(account); _shares[account] = shares_; _totalShares = _totalShares + shares_;
}
_addPayee是我们在构造函数中调用的用于设置收款人数组的函数。这个函数有两个参数,收款人的帐户和与其相关的份额数量。然后它会检查账户是否为零地址,份额是否大于零,以及该账户是否已经注册为收款人。如果所有检查都通过,那么我们将数据添加到各自的变量中。
现在让我们添加一个函数来支持将代币分发给收款人。
pragma solidity 0.8.0;function release(address account) public virtual { require( _shares[account] > 0, "TokenPaymentSplitter: account has no shares" ); uint256 tokenTotalReceived = IERC20(paymentToken).balanceOf(address(this)) + _totalTokenReleased; uint256 payment = (tokenTotalReceived * _shares[account]) / _totalShares - _tokenReleased[account]; require(payment != 0, "TokenPaymentSplitter: account is not due payment"); _tokenReleased[account] = _tokenReleased[account] + payment; _totalTokenReleased = _totalTokenReleased + payment; IERC20(paymentToken).safeTransfer(account, payment);}
Release是一个任何人都可以调用的函数,它接受一个现有收款人帐户的参数。来分析一下这个函数中发生了什么。首先,它检查帐户是否有分配给它的份额。然后,它创建一个名为tokenTotalReceived的变量,该变量将合约的当前代币余额与之前释放的代币总数相加。创建另一个称为payment的变量,该变量确定收到的代币总额中有多少是欠账户的,然后减去多少已经释放到账户。然后,一个require语句检查当前支付金额是否大于零(即,当前是否欠下了更多代币)。如果该检查通过,则更新账户的tokenReleased,并更新totalTokenReleased。最后,支付给账户的代币金额被转账。
现在函数已经就位了!但是这个合约还有一件事要做....事件!
我们将在合约中添加两个事件,将事件添加到合约顶部是一个良好的实践。
pragma solidity 0.8.0;event PayeeAdded(address account, uint256 shares);
event PaymentReleased(address to, uint256 amount);
合约中包含这些事件之后,我们将在适当的函数中发出它们。
pragma solidity 0.8.0;function _addPayee(address account, uint256 shares_) internal { ///existingFunctionCode emit PayeeAdded(account, shares_);}function release(address account) public virtual { ///existingFunctionCode emit PaymentReleased(account, payment);
}
现在代币支付拆分合约已经建立!为了理解这在真实场景中是如何工作的,让我们创建一个模拟池合约,它将导入代币支付拆分器。
创建模拟池合约
这个合约不会很复杂,因为我们只是想演示如何集成代币支付拆分器。这个合约定期收到我们想分发给收款人列表的特定ERC20代币。这个ERC20代币可以通过不同的场景到达,比如用户存款或来自另一个智能合约的重定向费用。在现实生活中,根据不同的项目,可能会有一个更复杂的合约,包含更多的功能来满足用户的用例。
在合约文件夹中,创建一个名为 MockPool.sol 的新文件。然后添加以下代码。
pragma solidity ^0.8.0;import "@openzeppelin/contracts/access/Ownable.sol";import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";import "./TokenPaymentSplitter.sol";contract MockPool is Ownable, TokenPaymentSplitter { using SafeERC20 for IERC20; constructor( address[] memory _payees, uint256[] memory _shares, address _paymentToken ) TokenPaymentSplitter(_payees, _shares, _paymentToken) {} function drainTo(address _transferTo, address _token) public onlyOwner { require( _token != paymentToken, "MockPool: Token to drain is PaymentToken" ); uint256 balance = IERC20(_token).balanceOf(address(this)); require(balance > 0, "MockPool: Token to drain balance is 0"); IERC20(_token).safeTransfer(_transferTo, balance); }
}
在这份合约中,导入三样东西。首先是OpenZeppelin的Ownable实用程序,它在某些函数上使用唯一的onlyOwner 修饰符。第二个是SafeERC20,它允许安全的ERC20代币转账,正如将在合约中看到。第三个是我们的TokenPaymentSplitter合约。
在MockPool构造函数中,我们需要TokenPaymentSplitter提供相同的三个参数,我们只是将它们传递给我们继承的合约。
在这个合约中添加了另一个函数,drainTo。它实际上与TokenPaymentSplitter合约没有任何关系。它只是在另一个没有设置为支付代币的ERC20代币被发送到池时的一种安全机制,然后有一种方法让合约所有者释放该代币。
测试合约
测试智能合约与创建它们同样重要。这些合约处理的资产通常是属于其他人的,所以作为开发人员,我们有责任确保这些资产按照他们应该的方式工作,并且我们的测试可以覆盖几乎所有的边缘情况。
将在这里进行的测试是一些示例,以显示TokenPaymentSplitter智能合约按照我们的预期工作。在处理自己的项目时,可能希望创建专门适合自己的用例的测试。
为了支持我们的测试,我们希望包含一个ERC20代币,为此,我们将创建一个新的solididity文件,该文件导入OpenZepplin ERC20模板以供我们的测试使用。在合约文件夹中,创建一个名为Imports.sol 的新文件,并包括以下代码:
pragma solidity ^0.8.0;import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
contract Imports {}
现在,在test文件夹中创建一个名为test.js的文件。在这个文件的顶部,我们将导入支持我们的测试的包。
const { expect } = require("chai")
const { ethers } = require("hardhat")
现在,为了设置测试,我们将首先创建必要的变量,创建beforeEach函数,该函数在每次测试之前调用,并创建一个空的 describe 函数,该函数将很快包含我们的测试。
describe("TokenPaymentSplitter Tests", () => {let deployerlet account1let account2let account3let account4let testPaymentTokenlet mockPoolbeforeEach(async () => { [deployer, account1, account2, account3, account4] = await ethers.getSigners() const TestPaymentToken = await ethers.getContractFactory("ERC20PresetMinterPauser") testPaymentToken = await TestPaymentToken.deploy("TestPaymentToken", "TPT") await testPaymentToken.deployed()})describe("Add payees with varying amounts and distribute payments", async () => {}
}
在这些部分就位后,让我们进入这些测试的核心部分!
支付代币平均分配给多个收款人
在我们的第一个测试中,我们想看看当我们部署一个包含平均分配份额的收款人列表的合约时会发生什么。下面是测试代码。
it("payment token is distributed evenly to multiple payees", async () => { payeeAddressArray = [account1.address, account2.address, account3.address, account4.address] payeeShareArray = [10, 10, 10, 10] const MockPool = await ethers.getContractFactory("MockPool") mockPool = await MockPool.deploy( payeeAddressArray, payeeShareArray, testPaymentToken.address ) await mockPool.deployed() await testPaymentToken.mint(mockPool.address, 100000) await mockPool .connect(account1) .release(account1.address) await mockPool .connect(account2) .release(account2.address) await mockPool .connect(account3) .release(account3.address) await mockPool .connect(account4) .release(account4.address) const account1TokenBalance = await testPaymentToken.balanceOf(account1.address) const account2TokenBalance = await testPaymentToken.balanceOf(account2.address) const account3TokenBalance = await testPaymentToken.balanceOf(account3.address) const account4TokenBalance = await testPaymentToken.balanceOf(account4.address) expect(account1TokenBalance).to.equal(25000) expect(account2TokenBalance).to.equal(25000) expect(account3TokenBalance).to.equal(25000) expect(account4TokenBalance).to.equal(25000)
})
在这个测试中,我们将合约分配给4个收款人,每个人都有10个相同的份额。然后我们向合约发送100000单位的testPaymentToken,并向每个收款人发放付款。在测试中可以注意到,每个收款人都在调用函数来向自己释放代币。
支付代币不均匀地分配给多个收款人
在第二个测试中,我们希望确保即使每个收款人的份额分配不均,数学计算仍然有效。
it("payment token is distributed unevenly to multiple payees", async () => { payeeAddressArray = [account1.address, account2.address, account3.address, account4.address] payeeShareArray = [10, 5, 11, 7] const MockPool = await ethers.getContractFactory("MockPool") mockPool = await MockPool.deploy( payeeAddressArray, payeeShareArray, testPaymentToken.address ) await mockPool.deployed() await testPaymentToken.mint(mockPool.address, 100000) await mockPool .connect(account1) .release(account1.address) await mockPool .connect(account2) .release(account2.address) await mockPool .connect(account3) .release(account3.address) await mockPool .connect(account4) .release(account4.address) const mockPoolTestPaymentTokenBalance = await testPaymentToken.balanceOf( mockPool.address ) const account1TokenBalance = await testPaymentToken.balanceOf(account1.address) const account2TokenBalance = await testPaymentToken.balanceOf(account2.address) const account3TokenBalance = await testPaymentToken.balanceOf(account3.address) const account4TokenBalance = await testPaymentToken.balanceOf(account4.address) expect(mockPoolTestPaymentTokenBalance).to.equal(1) expect(account1TokenBalance).to.equal(30303) expect(account2TokenBalance).to.equal(15151) expect(account3TokenBalance).to.equal(33333) expect(account4TokenBalance).to.equal(21212)
})
看起来收款人还能拿到钱,但注意到什么了吗?合约中还剩下一个单位的支付代币!由于Solidity没有小数,当它达到最低单位时,它通常会四舍五入,这可能会导致合约尘埃飞扬,就像我们在这里看到的。不过不用担心,因为我们预计未来会有支付代币流入合约,所以它将继续分发。
支付代币不均匀地分配给多个收款人,并将额外的支付代币发送到池中
这与之前的测试类似,不过在资金被释放给收款人之间增加了更多支付代币发送到池中。这表明,随着支付代币不断流入模拟池合约,数学仍然可以确保收款人收到正确的金额。
it("payment token is distributed unevenly to multiple payees with additional payment token sent to pool", async () => { payeeAddressArray = [account1.address, account2.address, account3.address, account4.address] payeeShareArray = [10, 5, 11, 7] const MockPool = await ethers.getContractFactory("MockPool") mockPool = await MockPool.deploy( payeeAddressArray, payeeShareArray, testPaymentToken.address ) await mockPool.deployed() await testPaymentToken.mint(mockPool.address, 100000) await mockPool .connect(account1) .release(account1.address) await mockPool .connect(account2) .release(account2.address) await testPaymentToken.mint(mockPool.address, 100000) await mockPool .connect(account3) .release(account3.address) await mockPool .connect(account4) .release(account4.address) await mockPool .connect(account1) .release(account1.address) await mockPool .connect(account2) .release(account2.address) const mockPoolTestPaymentTokenBalance = await testPaymentToken.balanceOf( mockPool.address ) const account1TokenBalance = await testPaymentToken.balanceOf(account1.address) const account2TokenBalance = await testPaymentToken.balanceOf(account2.address) const account3TokenBalance = await testPaymentToken.balanceOf(account3.address) const account4TokenBalance = await testPaymentToken.balanceOf(account4.address) expect(mockPoolTestPaymentTokenBalance).to.equal(1) expect(account1TokenBalance).to.equal(60606) expect(account2TokenBalance).to.equal(30303) expect(account3TokenBalance).to.equal(66666) expect(account4TokenBalance).to.equal(42424)
})
现在所有的测试都就绪了,是时候运行它们了,看看它们是否工作!在项目根文件夹中,使用npx hardhat test启动测试。如果一切都是正确的,那么你应该看到如下图所示的所有绿色格子。
如上所述,我们需要做更多的测试,以确保整个项目/协议按照预期工作,支付拆分器是它的集成部分。这将意味着更多的单元测试来覆盖所有可用的功能,以及更复杂的集成测试,这取决于具体用例。
总结
支付是许多加密协议的一个常见方面,有几种方法可以解决它们。今天我们学习了一种管理支付的方法,尽管用户甚至可以在此合约的基础上构建以满足您的特定需求,如跨多个代币启用支付,添加额外的收款人或移除收款人,或在一个函数调用中同时分发所有支付。
Source:https://medium.com/coinmonks/create-an-erc20-token-payment-splitting-smart-contract-c79436470ccc
查看更多