Soulbound Token
イーサリアムの提唱者ビタリック・ブリテンの論文
https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4105763
で話題となった”Soulbound Token(SBT)”。
簡単に言うと譲渡できないNFTというもののようですが、ブロックチェーンのまた新しい可能性を感じます。
ここでは、実際にそのトークンの実装を説明した動画をみて理解を深めたいと思います。(IDE(Remix)の使い方は以下と同様)
http://bitlife.me/bc/2022/04/29/
http://bitlife.me/bc/2022/01/09/
https://github.com/Dervoo/Soulbound-Token/blob/master/contracts/SoulboundToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
/**
* An experiment in Soul Bound Tokens (SBT's) following Vitalik's
* co-authored whitepaper at:
* https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4105763
*
* I propose for a rename to Non-Transferable Tokens NTT's
*/
contract SoulboundToken {
struct Soul {
uint256 patientIdentityNumber;
string identity; // name / surname
string town;
string street;
uint8 streetNumber;
string postCode;
string sicknessIdentity;
string lastTestRecord;
string findings;
string treatment;
string treatmentIndications;
string annotations;
string plannedVisits;
}
mapping (address => Soul) private souls; // Souls created by operator
mapping (address => mapping (address => Soul)) soulProfiles; // Anybody can create 1 profile for soul owners
mapping (address => address[]) private profiles; // Addresses of people who created profiles for a soul
string public name;
string public ticker; // operation identity
address public operator;
bytes32 private zeroHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
event Mint(address _soul);
event Burn(address _soul);
event Update(address _soul);
event SetProfile(address _profiler, address _soul);
event RemoveProfile(address _profiler, address _soul);
constructor(string memory _name, string memory _ticker) {
name = _name;
ticker = _ticker;
operator = msg.sender;
}
function mint(address _soul, Soul memory _soulData) external {
require(keccak256(bytes(souls[_soul].identity)) == zeroHash, "Soul already exists");
require(msg.sender == operator, "Only operator can mint new souls");
souls[_soul] = _soulData;
emit Mint(_soul);
}
function burn(address _soul) external {
require(msg.sender == _soul || msg.sender == operator, "Only users and issuers have rights to delete their data");
delete souls[_soul];
for (uint i=0; i<profiles[_soul].length; i++) {
address profiler = profiles[_soul][i];
delete soulProfiles[profiler][_soul];
}
emit Burn(_soul);
}
function update(address _soul, Soul memory _soulData) external {
require(msg.sender == operator, "Only operator can update soul data");
require(keccak256(bytes(souls[_soul].identity)) != zeroHash, "Soul does not exist");
souls[_soul] = _soulData;
emit Update(_soul);
}
function hasSoul(address _soul) external view returns (bool) {
if (keccak256(bytes(souls[_soul].identity)) == zeroHash) {
return false;
} else {
return true;
}
}
function getSoul(address _soul) external view returns (Soul memory) {
return souls[_soul];
}
/**
* Profiles are used by 3rd parties and individual users to store data.
* Data is stored in a nested mapping relative to msg.sender
* By default they can only store data on addresses that have been minted
*/
function setProfile(address _soul, Soul memory _soulData) external {
require(keccak256(bytes(souls[_soul].identity)) != zeroHash, "Cannot create a profile for a soul that has not been minted");
soulProfiles[msg.sender][_soul] = _soulData;
profiles[_soul].push(msg.sender);
emit SetProfile(msg.sender, _soul);
}
function getProfile(address _profiler, address _soul) external view returns (Soul memory) {
return soulProfiles[_profiler][_soul];
}
function listProfiles(address _soul) external view returns (address[] memory) {
return profiles[_soul];
}
function hasProfile(address _profiler, address _soul) external view returns (bool) {
if (keccak256(bytes(soulProfiles[_profiler][_soul].identity)) == zeroHash) {
return false;
} else {
return true;
}
}
function removeProfile(address _profiler, address _soul) external {
require(msg.sender == _soul, "Only users have rights to delete their profile data");
delete soulProfiles[_profiler][msg.sender];
emit RemoveProfile(_profiler, _soul);
}
}
Code language: JavaScript (javascript)
コードを見てみるとその内容がよく理解できます。
mapping (address => Soul) private souls; // Souls created by operator
mapping (address => mapping (address => Soul)) soulProfiles; // Anybody can create 1 profile for soul owners
この二行が特徴をよく説明しています。オリジナルのコードにはないですが、動画にあるコメントがわかりやすいので追記しました。
Soulという構造体のデータを発行できる人、焼却できる人、登録できる人、更新できる人、参照できる人、それぞれルールが決められています。
所有者が移動できないので構造はシンプルです。
コードの中でeventやzeroHashについても調べてみました。
eventについて
https://qiita.com/hakumai-iida/items/3da0252415ec24fe177b
詳しく説明されいます。サンプルDAppsについてとても興味深いです。
https://www.tutorialspoint.com/solidity/solidity_events.htm
コントラクトからフロントエンドへの通知の実装について書かれています。使い方がよくわかります。(スクショとりました)
zeroHashについて
bytes32 private zeroHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
..
require(keccak256(bytes(souls[_soul].identity)) == zeroHash, “Soul already exists”);
インスタンスがない状態、Nullのような状態を表すために使われているようです。
ためしにゼロのハッシュ値をとってみました。(Ethereum標準ハッシュ関数keccak256 ケチャック)
参考) https://qiita.com/kaito1994/items/62974800419f0e51c6cb
from Crypto.Hash import keccak
import binascii
keccak256 = keccak.new(data=0, digest_bits=256).digest()
print("Keccak256:", binascii.hexlify(keccak256))
#Keccak256: b'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'
Code language: PHP (php)
下記サイトでは、SBTついて詳しく書かれています。
https://www.xross-dx.com/article/aboutSBT-byGG-vol1.html
アドレスの所有者に固有の情報を与えことによって、信用調査等に利用できるといったところでしょうか。
情報を改竄できないブロックチェーンならではの使い方と言えます。