配当機能付きのトークンの実装方法【Solidity】
Expert - 専門2022年2月1日 00時00分
スマートコントラクトを使えば、トークン保有者に様々な権利を提供することができます。例えば、収益をトークンの保有者に配分するといった利用方法が考えられます。この記事では、トークンの所有者間でEthereum を比例配分できるERC-20準拠のシンプルな実装例をご紹介します。
SafeMath
コントラクト内で四則演算をする時に、値がオーバーフロー、アンダーフローしてしまう脆弱性に対応するために以下の関数を定義しておきます。
スマートコントラクトの制限
スマートコントラクトの開発において度々立ちはだかる問題の1つは「ガス代の高さ」です。この問題は、配当金を支払うときに顕著に現れます。配当金を送金する際は、すべてのトークン所有者に送金をする必要がありますが、トークン保有者は大量にいる可能性があり、それら全員に送金処理を繰り返し行うと莫大なコストがかかってしまい現実的ではありません。そのため、送金処理を毎回行うのではなく、ある一定金額が溜まったら送金を実行するというような工夫が必要になります。以前投稿したRepublic の記事にて、Republic Noteの保有者への配当が一定額に達するたびに行われる仕組みになっていたのもこれが理由でしょう。
そのために、今回のコントラクトでは、各アカウントに支払うべき配当金の状況を管理できるようにし、実際の送金は別のタイミングで行います。各アカウントごとの配当金は、以下のいずれかのステータスを持ちます。
すでに引き出しされた。すでにアカウントに入金しているが、まだ引き出されていない。まだアカウントに入金されていない。dividendPerToken
は、このコントラクトに預けられたトークンごとの累計金額(ETH)です。例えば、100個のトークンがあり、コントラクトが作成されてから200 ETHを集めていた場合、dividendPerToken は1トークンあたり2 ETHを表します。この値は決して減少することはなく、トークン所有者による金額の引き出しとは完全に独立していることに注意してください。
dividendBalanceOf
は、各アカウントに入金したものの、まだ引き出されていない金額を表すマッピングです。引き出しすると、この量は減少します。
dividendCreditedPerToken
は、以前にアカウントに入金された(つまりdividendBalanceOfに追加された)トークンごとのETHの累積量を表すマッピングです。
ERC-20 トークンコントラクトの拡張
配当機能を実装するために、ERC-20トークンを譲渡する関数(transfer
)を拡張して、各アカウントに支払うべき金額(dividendCreditedPerToken
と dividendBalanceOf
)を更新する必要があります。dividendCreditedTo
は、現在のdividendPerToken
の値に調整されます。その変更は、dividendBalanceOf
にクレジットされる必要がある値を表します。
このupdate
関数は、アカウントに最後にアクセスしたとき以降に受け取った配当金に関して、アカウントの値を最新にするものです。この関数をERC-20のtransfer
関数に追加して、送り手と受け手の両方のアカウントごとの残高を更新しましょう。
入金と出金
deposit
関数は、ETH を受け取り、グローバルなdividendPerToken
を更新します。
withdraw
関数は、支払うべき配当を更新し、それを送金します。
update(msg.sender)
は、msg.sender
の残高の最後の更新以降に収集された配当金に関して、 dividendBalanceOf
が最新であることを確認する役割を担います。
配当機能を実現するために必要な実装は以上になります。以下、コントラクト全体のコードです。
pragma solidity ^0.4.24;
contract SafeMath {
function add(uint a, uint b) public pure returns (uint c) {
c = a + b;
require(c >= a);
}
function sub(uint a, uint b) public pure returns (uint c) {
require(b <= a);
c = a - b;
}
function mul(uint a, uint b) public pure returns (uint c) {
c = a * b;
require(a == 0 || c / a == b);
}
function div(uint a, uint b) public pure returns (uint c) {
require(b > 0);
c = a / b;
}
}
contract ERC20Interface {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
contract ApproveAndCallFallBack {
function receiveApproval(address from, uint256 tokens, address token, bytes data) public;
}
contract SimpleDividenedToken is ERC20Interface, SafeMath {
string public symbol;
string public name;
uint8 public decimals;
uint public _totalSupply;
mapping(address => uint) balances;
mapping(address => mapping(address => uint)) allowed;
constructor() public {
symbol = "SDT";
name = "SimpleDividend Coin";
decimals = 2;
_totalSupply = 100000;
balances[0xAb1008Ad4c79fE169516B42DfB5ac661af0B5934] = _totalSupply;
emit Transfer(address(0), 0xAb1008Ad4c79fE169516B42DfB5ac661af0B5934, _totalSupply);
}
mapping(address => uint256) dividendBalanceOf;
uint256 public dividendPerToken;
mapping(address => uint256) dividendCreditedTo;
function balanceOf(address tokenOwner) public constant returns (uint balance) {
return balances[tokenOwner];
}
function update(address account) internal {
uint256 owed = sub(dividendPerToken, dividendCreditedTo[account]);
dividendBalanceOf[account] = add(dividendBalanceOf[account], mul(balanceOf(account), owed));
dividendCreditedTo[account] = dividendPerToken;
}
function totalSupply() public constant returns (uint) {
return _totalSupply - balances[address(0)];
}
function transfer(address to, uint tokens) public returns (bool success) {
update(msg.sender);
update(to);
balances[msg.sender] = sub(balances[msg.sender], tokens);
balances[to] = add(balances[to], tokens);
emit Transfer(msg.sender, to, tokens);
return true;
}
function approve(address spender, uint tokens) public returns (bool success) {
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
return true;
}
function transferFrom(address from, address to, uint tokens) public returns (bool success) {
update(from);
update(to);
balances[from] = sub(balances[from], tokens);
allowed[from][msg.sender] = sub(allowed[from][msg.sender], tokens);
balances[to] = add(balances[to], tokens);
emit Transfer(from, to, tokens);
return true;
}
function deposit() public payable {
dividendPerToken = add(dividendPerToken, div(msg.value, _totalSupply));
}
function withdraw() public {
update(msg.sender);
uint256 amount = dividendBalanceOf[msg.sender];
dividendBalanceOf[msg.sender] = 0;
msg.sender.transfer(amount);
}
function allowance(address tokenOwner, address spender) public constant returns (uint remaining) {
return allowed[tokenOwner][spender];
}
function approveAndCall(address spender, uint tokens, bytes data) public returns (bool success) {
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
ApproveAndCallFallBack(spender).receiveApproval(msg.sender, tokens, this, data);
return true;
}
function () public payable {
revert();
}
}
まとめ
今回は、トークン保有者に収益を分配をするためのシンプルなスマートコントラクトの実装方法を紹介しました。スマートコントラクトの開発においては、ガス代がかかるということを念頭に実装方法を考える必要があります。また、サービス設計上ではコントラクトに書く必要のない取引についてはオフチェーンで管理した方が良い場合もあります。例えば、Polygon 上で作られた SunflowerFarmer というゲームでは、全てのトランザクションをオンチェーン上に書いたことでPolygon内のトランザクションのほとんどを占め詰まってしまったということが起きました。チェーンの特性を理解して、サービス全体としてどのようにコントラクトを実装するか検討しましょう。