在加密貨幣的幾乎每個領域,支付都是一個反復出現的話題,特別是向多個質押者提供支付。例如,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

 

關於

ChinaDeFi - ChinaDeFi.com 是一個研究驅動的DeFi創新組織,同時我們也是區塊鏈開發團隊。每天從全球超過500個優質信息源的近900篇內容中,尋找思考更具深度、梳理更為系統的內容,以最快的速度同步到中國市場提供決策輔助材料。

Layer 2道友 - 歡迎對Layer 2感興趣的區塊鏈技術愛好者、研究分析人與Gavin(微信: chinadefi)聯繫,共同探討Layer 2帶來的落地機遇。敬請關注我們的微信公眾號 “去中心化金融社區”。

閱讀原文