區塊鏈研究實驗室|如何使用solidity實現staking功能

買賣虛擬貨幣

當某人投資某些資產以換取某種程度的控制,影響力或參與其活動時,就會有人持有該企業的股份。

在加密貨幣世界中,這被理解為只要他們不轉讓他們擁有的某些代幣,就會給予使用者某種權利或獎勵。staking機制通常鼓勵對代幣交易進行代幣持有,而代幣交易又有望推動代幣估值。

要建立這種staking機制,我們需要:

  1. staking代幣

  2. 跟蹤stakes、權益持有者和回報的資料結構

  3. 建立和刪除stakes的方法

  4. 獎勵機制

我們繼續吧。

staking代幣

為我們的staking代幣建立ERC20代幣。稍後我將需要SafeMash和Ownable,所以讓我們匯入並使用它們。

pragma solidity ^0.5.0;

import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";

/**
@title Staking Token (STK)
@author Alberto Cuesta Canada
@notice Implements a basic ERC20 staking token with incentive distribution.
*/

contract StakingToken is ERC20, Ownable {
   using SafeMath for uint256;

/**
    * @notice The constructor for the Staking Token.
    * @param _owner The address to receive all tokens on construction.
    * @param _supply The amount of tokens to mint on construction.
    */

   constructor(address _owner, uint256 _supply)
public
   {
       _mint(_owner, _supply);
   }

就這樣,不需要別的了。

權益持有者

在這個實施過程中,我們將跟蹤權益持有者,以便日後有效地分配激勵措施。理論上,可能無法像普通的erc20代幣那樣跟蹤它們,但在實踐中,很難確保權益持有者不會在不跟蹤它們的情況下與分發系統進行博弈。

為了實現,我們將使用一個權益持有者地址的動態陣列。

/**
     * @notice We usually require to know who are all the stakeholders.
     */

    address[] internal stakeholders;

以下方法新增權益持有者,刪除權益持有者,並驗證地址是否屬於權益持有者。 其他更有效的實現肯定是可能的,但我喜歡這個可讀性。

/**
    * @notice A method to check if an address is a stakeholder.
    * @param _address The address to verify.
    * @return bool, uint256 Whether the address is a stakeholder,
    * and if so its position in the stakeholders array.
    */

functionisStakeholder(address _address)
public
view
returns(bool, uint256)
{
for (uint256 s = 0; s < stakeholders.length; s += 1){
if (_address == stakeholders[s]) return (true, s);
       }
return (false0);
   }

/**
    * @notice A method to add a stakeholder.
    * @param _stakeholder The stakeholder to add.
    */

functionaddStakeholder(address _stakeholder)
public
{
       (bool _isStakeholder, ) = isStakeholder(_stakeholder);
if(!_isStakeholder) stakeholders.push(_stakeholder);
   }

/**
    * @notice A method to remove a stakeholder.
    * @param _stakeholder The stakeholder to remove.
    */

functionremoveStakeholder(address _stakeholder)
public
{
       (bool _isStakeholder, uint256 s) = isStakeholder(_stakeholder);
if(_isStakeholder){
           stakeholders[s] = stakeholders[stakeholders.length - 1];
           stakeholders.pop();
       }
   }

抵押

最簡單形式的抵押將需要記錄抵押規模和權益持有者。 一個非常簡單的實現可能只是從權益持有者的地址到抵押大小的對映。

/**
    * @notice The stakes for each stakeholder.
    */

   mapping(address => uint256) internal stakes;

我將遵循erc20中的函式名,並建立等價物以從抵押對映中獲取資料。

/**
    * @notice A method to retrieve the stake for a stakeholder.
    * @param _stakeholder The stakeholder to retrieve the stake for.
    * @return uint256 The amount of wei staked.
    */

functionstakeOf(address _stakeholder)
public
view
returns(uint256)
{
return stakes[_stakeholder];
   }

/**
    * @notice A method to the aggregated stakes from all stakeholders.
    * @return uint256 The aggregated stakes from all stakeholders.
    */

functiontotalStakes()
public
view
returns(uint256)
{
       uint256 _totalStakes = 0;
for (uint256 s = 0; s < stakeholders.length; s += 1){
           _totalStakes = _totalStakes.add(stakes[stakeholders[s]]);
       }
return _totalStakes;
   }

我們現在將給予STK持有者建立和移除抵押的能力。我們將銷燬這些令牌,因為它們被標記,以防止使用者在移除標記之前轉移它們。

請注意,在建立抵押時,如果使用者試圖放置比他擁有的更多的令牌時,_burn將會恢復。在移除抵押時,如果試圖移除更多的已持有的代幣,則將恢復對抵押對映的更新。

最後,我們使用addStakeholder和removeStakeholder來記錄誰有抵押,以便稍後在獎勵系統中使用。

/**
    * @notice A method for a stakeholder to create a stake.
    * @param _stake The size of the stake to be created.
    */

functioncreateStake(uint256 _stake)
public
{
       _burn(msg.sender, _stake);
if(stakes[msg.sender] == 0) addStakeholder(msg.sender);
       stakes[msg.sender] = stakes[msg.sender].add(_stake);
   }

/**
    * @notice A method for a stakeholder to remove a stake.
    * @param _stake The size of the stake to be removed.
    */

functionremoveStake(uint256 _stake)
public
{
       stakes[msg.sender] = stakes[msg.sender].sub(_stake);
if(stakes[msg.sender] == 0) removeStakeholder(msg.sender);
       _mint(msg.sender, _stake);
   }

獎勵

獎勵機制可以有許多不同的實現,並且執行起來相當繁重。對於本合同,我們將實施一個非常簡單的版本,其中權益持有者定期獲得相當於其個人抵押1%的STK代幣獎勵。

在更復雜的合同中,當滿足某些條件時,將自動觸發獎勵的分配,但在這種情況下,我們將讓所有者手動觸發它。按照最佳實踐,我們還將跟蹤獎勵並實施一種撤銷方法。

和以前一樣,為了使程式碼可讀,我們遵循ERC20.sol合同的命名約定,首先是資料結構和資料管理方法:

/**
    * @notice The accumulated rewards for each stakeholder.
    */

   mapping(address => uint256) internal rewards;

/**
    * @notice A method to allow a stakeholder to check his rewards.
    * @param _stakeholder The stakeholder to check rewards for.
    */

functionrewardOf(address _stakeholder)
public
view
returns(uint256)
{
return rewards[_stakeholder];
   }

/**
    * @notice A method to the aggregated rewards from all stakeholders.
    * @return uint256 The aggregated rewards from all stakeholders.
    */

functiontotalRewards()
public
view
returns(uint256)
{
       uint256 _totalRewards = 0;
for (uint256 s = 0; s < stakeholders.length; s += 1){
           _totalRewards = _totalRewards.add(rewards[stakeholders[s]]);
       }
return _totalRewards;
   }

接下來是計算,分配和取消獎勵的方法:

/**
    * @notice A simple method that calculates the rewards for each stakeholder.
    * @param _stakeholder The stakeholder to calculate rewards for.
    */

functioncalculateReward(address _stakeholder)
public
view
returns(uint256)
{
return stakes[_stakeholder] / 100;
   }

/**
    * @notice A method to distribute rewards to all stakeholders.
    */

functiondistributeRewards()
public
onlyOwner
{
for (uint256 s = 0; s < stakeholders.length; s += 1){
           address stakeholder = stakeholders[s];
           uint256 reward = calculateReward(stakeholder);
           rewards[stakeholder] = rewards[stakeholder].add(reward);
       }
   }

/**
    * @notice A method to allow a stakeholder to withdraw his rewards.
    */

functionwithdrawReward()
public
{
       uint256 reward = rewards[msg.sender];
       rewards[msg.sender] = 0;
       _mint(msg.sender, reward);
   }

測試

沒有一套全面的測試,任何合同都不能完成。我傾向於至少為每個函式生成一個bug,但通常情況下不會發生這樣的事情。

除了允許您生成有效的程式碼之外,測試在開發設定和使用智慧合約的過程中也非常有用。

下面介紹如何設定和使用測試環境。我們將製作1000個STK令牌並將其交給使用者使用該系統。我們使用truffle進行測試,這使我們可以使用帳戶。

contract('StakingToken', (accounts) => {
   let stakingToken;
const manyTokens = BigNumber(10).pow(18).multipliedBy(1000);
const owner = accounts[0];
const user = accounts[1];

   before(async () => {
       stakingToken = await StakingToken.deployed();
   });

   describe('Staking', () => {
       beforeEach(async () => {
           stakingToken = await StakingToken.new(
               owner,
               manyTokens.toString(10)
           );
       });

在建立測試時,我總是編寫使程式碼恢復的測試,但這些測試並不是很有趣。 對createStake的測試顯示了建立賭注需要做些什麼,以及之後應該改變什麼。

重要的是要注意在這個抵押合約中我們有兩個平行的資料結構,一個用於STK餘額,一個用於抵押以及它們的總和如何透過建立和刪除抵押數量保持不變。在這個例子中,我們給使用者3個STK wei,該使用者的餘額加抵押總和將始終為3。

    it('createStake creates a stake.'async () => {
await stakingToken.transfer(user, 3, { from: owner });
await stakingToken.createStake(1, { from: user });

assert.equal(await stakingToken.balanceOf(user), 2);
assert.equal(await stakingToken.stakeOf(user), 1);
assert.equal(
await stakingToken.totalSupply(), 
               manyTokens.minus(1).toString(10),
           );
assert.equal(await stakingToken.totalStakes(), 1);
       });

對於獎勵,下面的測試顯示了所有者如何激發費用分配,使用者獲得了1%的份額獎勵。

       it('rewards are distributed.'async () => {
await stakingToken.transfer(user, 100, { from: owner });
await stakingToken.createStake(100, { from: user });
await stakingToken.distributeRewards({ from: owner });

           assert.equal(await stakingToken.rewardOf(user), 1);
           assert.equal(await stakingToken.totalRewards(), 1);
       });

當獎勵分配時,STK的總供應量會增加,這個測試顯示了三個資料結構(餘額、抵押和獎勵)是如何相互關聯的。現有和承諾的STK金額將始終是建立時的金額加上獎勵中分配的金額,這些金額可能會或可能不會被鑄造。。建立時生成的STK數量將等於餘額和抵押的總和,直到完成分配。

       it('rewards can be withdrawn.'async () => {
await stakingToken.transfer(user, 100, { from: owner });
await stakingToken.createStake(100, { from: user });
await stakingToken.distributeRewards({ from: owner });
await stakingToken.withdrawReward({ from: user });

const initialSupply = manyTokens;
const existingStakes = 100;
const mintedAndWithdrawn = 1;

assert.equal(await stakingToken.balanceOf(user), 1);
assert.equal(await stakingToken.stakeOf(user), 100);
assert.equal(await stakingToken.rewardOf(user), 0);
assert.equal(
await stakingToken.totalSupply(),
               initialSupply
                   .minus(existingStakes)
                   .plus(mintedAndWithdrawn)
                   .toString(10)
               );
assert.equal(await stakingToken.totalStakes(), 100);
assert.equal(await stakingToken.totalRewards(), 0);
       });

結論

抵押和獎勵機制是一種強大的激勵工具,複雜程度根據我們自身設計相關。erc20標準和safemath中提供的方法允許我們用大約200行程式碼對其進行編碼。

描下方二維碼新增我,拉您進入技術交流群

關注一下,精彩不停

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;