創建 ERC20 代幣支付拆分智能合約

2021-09-17 18:09:40

在加密貨幣的幾乎每個領域,支付都是一個反復出現的話題,特別是向多個質押者提供支付。例如,DAO希望為多個計劃提供資金,DEX希望合並向某些參與者分配交易費用,或者團隊希望將代幣作為月薪分發給團隊成員。

智能合約使我們能夠自動化這些類型的支付功能,這就限制了人工管理支付所導致的潛在錯誤,並允許我們將寶貴的時間花在其他生產性任務上。

今天,我們將學習如何創建自己的ERC20代幣支付拆分器,它可以合並到任何項目中!

先決條件和設置

下面的內容要求你對Solidity有點熟悉,不過任何人都可以學習。

項目架構

我們將創建兩個合約。第一個將是ERC20代幣支付拆分智能合約,第二個將是模擬池智能合約。ERC20代幣支付拆分器智能合約將是抽象的,並持有用於管理收付方及其各自支付部分的邏輯和數據。模擬池將繼承ERC20代幣支付拆分器,以便我們可以自動將支付分發給多個質押者。在兩個合約中拆分支付功能的原因有兩個:

  • 展示在真實世界的用例中代幣支付拆分合約的使用

  • 確保代幣支付拆分合約足夠靈活,任何人都可以選擇並集成到自己的項目中

OpenZeppelin已有一個名為PaymentSplitter.sol的智能合約。用於以太坊支付拆分。我們將利用這個現有的功能並對其進行定制,使其能夠與ERC20代幣一起工作。

設置开發環境

本教程中的工具:

  • 安全帽——智能合約开發環境

  • OpenZeppelin -經過審計的智能合約模板

現在在一個空目錄中使用NPM init -y啓動一個NPM項目

設置項目後,使用以下命令安裝 Hardhat:

創建 ERC20 代幣支付拆分智能合約

在安裝了Hardhat之後,輸入npx Hardhat並選擇創建基本示例項目的選項。這將包括一個方便的文件結構,可以輕松地創建、測試和部署您自己的合約。

創建 ERC20 代幣支付拆分智能合約

選擇創建基本示例項目

可以刪除contract 文件夾中的Greeter.sol文件,並從test文件夾中刪除sample-test.js文件。

我們還將安裝安全帽插件庫,它們是Hardhat插件。它們允許我們添加用於測試和部署智能合約的工具。

創建 ERC20 代幣支付拆分智能合約

在hardhat.config.js文件的頂部,添加

創建 ERC20 代幣支付拆分智能合約

需安裝一個叫chai的包,用來測試我們的智能合約。

創建 ERC20 代幣支付拆分智能合約

需安裝OpenZeppelin合約庫。

創建 ERC20 代幣支付拆分智能合約

創建代幣支付拆分器

這個代幣支付拆分智能合約將提供邏輯來設置和存儲涉及收款人列表和每個收款人份額的數據。每個收款人持有的份額數等於他們應該獲得的資金比例(例如,如果有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.0
constructor(
   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.0
function 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 deployer
let account1
let account2
let account3
let account4
let testPaymentToken
let mockPool
beforeEach(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啓動測試。如果一切都是正確的,那么你應該看到如下圖所示的所有綠色格子。

創建 ERC20 代幣支付拆分智能合約

如上所述,我們需要做更多的測試,以確保整個項目/協議按照預期工作,支付拆分器是它的集成部分。這將意味着更多的單元測試來覆蓋所有可用的功能,以及更復雜的集成測試,這取決於具體用例。

總結

支付是許多加密協議的一個常見方面,有幾種方法可以解決它們。今天我們學習了一種管理支付的方法,盡管用戶甚至可以在此合約的基礎上構建以滿足您的特定需求,如跨多個代幣啓用支付,添加額外的收款人或移除收款人,或在一個函數調用中同時分發所有支付。

Source:https://medium.com/coinmonks/create-an-erc20-token-payment-splitting-smart-contract-c79436470ccc

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

推薦文章

解析五大交易所上幣:什么賽道最受歡迎?上幣效應究竟多強?

@OdailyChina @Assassin_Malvo Nerio(小寫)自上线幣安以來,已斬獲...

星球日報
4 8小時前

Arthur Hayes Token2049演講全文:市場在降息後可能崩盤,但以太坊表現可能會不錯

整理:Weilin,PANews “It’s fucking fed day”, 9 月 18 日...

星球日報
4 8小時前

深度解析Multi-Agent:Web3與AI終將相互成就?

如果說 AIGC 开啓了內容生成的智能時代,那么 AI Agent 則有機會把 AIGC 的能力真...

星球日報
4 8小時前

EIGEN解鎖在即?淺析EigenLayer當下估值和獲利預期

原創 | Odaily星球日報( @OdailyChina ) 作者|Azuma( @azuma_...

星球日報
4 8小時前

死而復生的“Neiro”,把幣安和何一都拉下水

在一周前,陀螺財經曾撰寫過《 交易所站隊“NEIRO”?MEME內战進行時 》,提到關於交易所站隊...

陀螺財經
4 8小時前

應對美聯儲降息:市場波動中的期權交易策略

本周市場顯示出看漲樂觀情緒,主要是由於加密貨幣交易者關注的積極因素——利率下調和流動性,將由美聯儲...

星球日報
4 8小時前