不動産ブロックチェーンの情報ハブ
Staking の実装と不動産分野での活用

Staking の実装と不動産分野での活用

Fumiya
Fumiya
Advanced - 上級2022年5月5日 00時00分

Proof of Stake (PoS) という言葉は聞いたことがあることが多いかもしれません。ステーキングとはトークンをネットワークに預けることで報酬を得ることです。 今回は以前の記事の配当機能付き ERC-20 トークンが継承できる形を想定して、ステーキングの機能を実装していきます。

この投稿について

この投稿では「ステーキングというと難しい処理をしているように感じる」という印象を「ステーキングの基本は『だれが・いつ・いくらステークしたのかを保持し、引き出す際に計算する』だけである」という印象に変えることを焦点にして解説しています。

最後のまとめで不動産についても言及していますので、「技術的な内容は難しい...」という方はまとめまで飛んでいただければと思います。

この投稿で扱うのは一つの実装例に過ぎないということにはご留意ください。実際にはステーキングの実装はこれよりも煩雑になり得ます。多くの場合、本質的にはステーキングには連想配列を利用します。連想配列とは特定のキー情報に対応するデータを保持できるプログラムの構造です。今回の場合はそのキーはウォレットアドレスになります。技術的には Solidity の場合は Mapping を利用することになりますが、アドレスに対応する「いつ・だれが・いくら」ステーキングをしたのかの情報が保存されます。この基本構造によってトークンが該当コントラクトに預けられ、配当とともに引き出せることになります。

トークンをスマートコントラクトに預ける

下記はこれからみていくコードの雛形になります。Stakable.sol のようなファイルを作り、トークンのコントラクトでインポートする想定です。

pragma solidity ^0.8.13;
contract Stakeable {
  constructor() {
     stakeholders.push(); // index 0 のバグを踏まない様に追加
  }

  struct Stake{
    address user; // ステークしたユーザーのアドレス
    uint256 amount; // ステークされた値
    uint256 since; // ステークされた時間(タイムスタンプ)
  }

  struct Stakeholder{
    address user;
    Stake[] address_stakes; // 現在のステーク (上記ステーク履歴の情報)
  }

  Stakeholder[] internal stakeholders; // Contractで実行されたすべてのステークを格納する配列。特定のインデックスに格納される。
  mapping(address => uint256) internal stakes; // 上記「特定のインデックス」はこちらのマッピングに格納

  event Staked(address indexed user, uint256 amount, uint256 index, uint256 timestamp); // トークンをステークするたびにイベントが発生する
}
stakable.sol
solidity

続いて、_addStakeholder 関数を作成します。こちらも内部から呼び出されることを想定されています。具体的には新規でステークする人の情報をコントラクト内部で作成するための関数です。

ガス代を安くするために Loop を使わずにインデックスを持たせ、値を直接参照できる様にしています。

function _addStakeholder(address staker) internal returns (uint256){ // ステークホルダーに追加する関数
	  stakeholders.push(); // 配列に新しく追加
	  uint256 userIndex = stakeholders.length - 1; // インデックスの計算
	  stakeholders[userIndex].user = staker; // 上記のインデックスの構造体のアドレスに値を設定
	  stakes[staker] = userIndex; // 該当アドレスに対する構造体への配列をマップ
	  return userIndex; 
  }
solidity

続いて、ステーキング関数です。

function _stake(uint256 _amount) internal{ // 送信者のためにステークを作成するために使用されます。これは、ステイカーのアカウントからステークされた金額を取り除き、そのトークンをステークコンテナ内に配置します。
    require(_amount > 0, "Cannot stake nothing"); // ステークの値は必ず0より上
    uint256 index = stakes[msg.sender]; // アドレスの確認
    uint256 timestamp = block.timestamp; // 現在のブロックのタイムスタンプ

    // ステイカーが既にステイクしたインデックスを持っているか、初めてステイクしたかを確認
    if(index == 0){
    // もし初めての場合、マッピングに情報を追加する必要がある
      index = _addStakeholder(msg.sender);
    }

    // 新しいStakeをプッシュします。
    stakeholders[index].address_stakes.push(Stake(msg.sender, _amount, timestamp));

    // ステークが発生したことを示すイベントを発信
    emit Staked(msg.sender, _amount, index,timestamp);
  }
solidity

ここで注意すべきことは、ここまでの関数ではどの口座の残高も変更していないことです。また、こちらはスマートコントラクト内部から呼び出されることを想定しています。例えば、100トークンしか持っていないのに1000トークンをステーキングしようとすると問題が起きますよね。そのチェックはトークンのコントラクトから行う必要があります。このコードは継承されることを念頭に書かれており、外部に晒されることになる stake 関数はトークンの実装本体で実装することになります。

そのため次に配当付きトークンの中に外部に公開される以下の関数を実装します。

function stake(uint256 _amount) public {
  require(_amount < _balances[msg.sender], "持分以上の額をステークすることはできません。");
  _stake(_amount);
  _burn(msg.sender, _amount); // トークンホルダーのステークしたトークンをバーンします
}
solidity

ステークした人に報酬を与え、引き出させる

上記の実装で無事にステーキングの実装は完了です。しかしこれではただお金を預けるだけのコントラクトです。実際にはトークンを預けたことによる報酬がなければステークをするモチベーションは生まれません。続いて報酬の仕組みを考えていきます。

ここまでの実装は、ステーキングをすると持っていたトークンがバーンされ、「バーンされた分のトークンがステークされた」という事実が記録としてマッピングに記録されるところまででした。今度はかけた額をトークンのコントラクトに呼び戻す部分を実装します。

function withdrawStake(uint256 amount, uint256 stake_index)  public {
  uint256 amount_to_mint = _withdrawStake(amount, stake_index);
  _mint(msg.sender, amount_to_mint);
}
solidity

上記は withdrawStake をトークンのコントラクトに実装したものになります。こちらも stake 関数と同じく外部に公開されます。こちらの関数では _withdrawStake で諸々のチェックと配列およびマッピングの値を操作した後に _mint 関数を実行し、預けた金額に報酬を加えた額の通貨が発行されます。

それではこの操作の本質である _withdrawStake とそれに関連する設定をみていきます。

報酬率と報酬計算

今回は時間当たり 0.5% の報酬を与えるように設定します。しかしこの報酬は永遠に 0.5% とは限りませんので、値は報酬率制御のために可変であり再代入可能である必要があります。Solidity の場合少数を扱いませんので、この場合は以下の様に変数に 5000 を代入しています(×0.005 は ÷5000と同等)。

この数字をベースに calculateReward を実装します。この計算は内部の状態を何も変化させないため、view 修飾子もあるため、お金を消費せずに実行することが可能です。

今回の場合時間当たりの報酬は 0.5% になっていますが、以下のアルゴリズムでは便宜的に一律のリワードで計算するようにしています。ここは償還期間を定めたステークを適宜提供したり、ステーク開始時の値をコントラクト内部に保持することによって柔軟に実装することができます。

uint256 internal rewardPerHour = 5000;

function calculateReward(Stake memory _current_stake) internal view returns(uint256){
		return (((block.timestamp - _current_stake.since) / 1 hours) * _current_stake.amount) / rewardPerHour;
}
solidity

報酬の引き出し

ここまで、「預け入れ」と「報酬計算」そして「報酬を引き出すためのトークン側のインタフェース」を実装してきました。引き出し時には「だれが・いつ・いくら」預け入れをしたのかというデータにアクセスする必要があるほか、「そのステークが何番目にユーザーにとってステークされたものなのか」にも留意する必要があります。そのため、引き出す際には明示的に「何番目」のインデックスなのかを示してあげる必要があります。

function _withdrawStake(uint256 amount, uint256 index) internal returns(uint256){
  // ユーザー及び、何番目のステーキングをいくら引き出すのか指定する
  uint256 user_index = stakes[msg.sender];
  Stake memory current_stake = stakeholders[user_index].address_stakes[index];
  require(current_stake.amount >= amount, "預けているお金以上に引き出すことはできません");
  
   // リワードの計算
   uint256 reward = calculateReward(current_stake);

   // 引き出す金額を、該当ステークから差し引く
   current_stake.amount = current_stake.amount - amount;
   if(current_stake.amount == 0){
   // 引き算の結果現在ステーキングしている金額が0になった場合、ステーキングの設定ごと削除する
     delete stakeholders[user_index].address_stakes[index];
   }else {
     // 0でない場合は新しい値で置き換える
     stakeholders[user_index].address_stakes[index].amount = current_stake.amount;
     // ステーキングの時間をリセットする
     stakeholders[user_index].address_stakes[index].since = block.timestamp;    
   }
   return amount+reward;
}
solidity

まとめ

今回はステーキングを実装していきましたが、印象は「ステーキングというと難しい処理をしているように感じる」から「ステーキングの基本は『だれが・いつ・いくらステークしたのかを保持し、引き出す際に計算する』だけである」に変わりましたでしょうか。

ステーキングというと銀行にお金を預けて利子をもらうことを想像しがちですが、実装においてステーキングはトークンの移動ではなくトークンのバーンとミントで表現されます。一つの理由はその方がガス代が安いからでもありますが、この点は暗号資産の取引においては少し考え方を変える必要があるところです。トークンはそれ自体が造幣局になるので、実際には日銀自体にお金を預けたら、報酬を含め得た新券が発行されるイメージに近いです。

この仕組みはインフレ率に合わせてバリデーターの人に報酬を与えるために実装されていますが、高すぎる報酬設計はトークンの総供給量を著しく増やす結果になり、インフレを引き起こします。そのためバランスを取るためにデフレを同時に起こすために運営側でトークンのバーンをすることになりますが、その匙加減は議論が分かれます。手数料をバーンすることは総供給量を制限することにつながりますが、トークンの流動性によってはインフレ圧力の方が強くなってしまうことが考えられます。

不動産分野での活用

一般的にトークン自体はERC-20などの標準に沿ったプログラム群でしかないため、そのトークンは不動産持分を表すこともできますし、ガバナンストークンにもなり得ます。その他の例を挙げると ○○ to Earn で代表的な STEPN は GMT と GST という異なる代替可能トークンを用いて価格の調整を行ってエコシステムのバランスをとっています。このように同じような仕組みのトークンが違う目的で使われることになるため、ステーキングの実装事態もそのトークンの特徴や総供給量、流動性に合わせて適切なものが選ばれる必要があります。

RealT や Landshare のような不動産トークンに関しては、多くの場合で総供給量が固定になっているケースが多いことが観測できます。これは現実の不動産持分の概念と一致させていることや、そもそもの実売価格と対応させることなどが背景として挙げられます。この世界観に総供給量の変化の概念を持ち込むと概念が非常に煩雑になるでしょう。総供給量が限られるであろう不動産トークンにおいて、ステーキングなどの「預け入れにかかる報酬」のモデルを実現しながら、参画者によってガバナンスを効かせるためには何かしらの工夫が必要であると言えます。

当然のことながら、不動産トークンが特定のプラットフォームで実現された場合には、そのプラットフォーム自体は一般的なガバナンストークンのような代替可能トークンを持つことになります。それらのトークンが「特定の不動産を表すトークン」の所有者に対してどの様な報酬を設定するのかも一つの焦点になるでしょう。これがどのようなトークンのセットによって実現されるのか、またそれらが本当に “Web3 らしい” のかについては、今後の開発が待たれるところです。

* OSSで公開されているコードも解説に利用していますので、気になる方はそちらもご覧ください。

Read Next