【Ethereum】【Solidity】OpenZeppelinのShareableのソースを読んでみる
Solidity言語を使って実装された、 基本的なContractをフォーマット化したOpenZeppelinというフレームワークが開発されています。
このフレームワークに含まれるShareable
というContractのソースを読んでみたので、メモを記載します。
Shareableコントラクトは、コントラクトの実行権を複数のオーナーで管理するためのコントラクトです。
このContractを継承すると、オーナーの実行制御を付与するためのmodifierを関数に定義することができます。(onlyOwner
,onlymanyowners
)
ソース
https://github.com/OpenZeppelin/zeppelin-solidity/blob/v1.2.0/contracts/ownership/Shareable.sol
コンストラクタ
function Shareable(address[] _owners, uint256 _required) { // contract登録者をownerとして登録する // オーナーのindexは後述の処理でビットフラグとして使用される。 // その為、はじめの格納位置は0ではなく1からになる owners[1] = msg.sender; // 逆引き用の配列にindex(1)をセット ownerIndex[msg.sender] = 1; // 初期引数で渡されるオーナーを登録する for (uint256 i = 0; i < _owners.length; ++i) { owners[2 + i] = _owners[i]; ownerIndex[_owners[i]] = 2 + i; } required = _required; require(required <= owners.length); }
- コンストラクタでは、オーナーの登録と必要なindexパラメタの初期化を行います。
_owners
には、共同管理するオーナーアドレスの配列を指定します。_required
には、実行に必要なオーナー数(承認数)を指定します。owners
はオーナーのアドレスを管理するフィールド変数です。address[256] owners;
- 最大255人(初めにindex1をつかっているので255)のオーナーを登録できます。
ownerIndex
は、アドレスからオーナーのindexを逆引きするためのフィールドです。mapping(address => uint256) ownerIndex;
- [気になった点]初めに仮引数で渡されるownersを登録していますが、senderを含めていないためowndersにsenderと同じアドレスがいると上手く動かなそうに見えます。
_owners
配列自体もそもそも重複チェックしていないため、_ownersに重複addressが存在すると逆引きのownerIndexがズレてしまう気がします。
onlymanyowners modifier
modifier onlymanyowners(bytes32 _operation) { if (confirmAndCheck(_operation)) { _; } }
- このmodifierを使用して、複数の登録オーナーが関数を承認(実行)しないと実行されない関数を実現できます。
_operation
には実行許可の操作を管理するためのバイトコードが渡されます。confirmAndCheck
は内部で実行したオーナーが_operation
の操作に対して実行(承認)を行ったかを記録しています。- その為、このmodifireを指定した関数は、規定数に達するまでオーナーからの実行を拒否することができます。
confirmAndCheck
function confirmAndCheck(bytes32 _operation) internal returns (bool) { // 関数実行者からindexを逆引き uint256 index = ownerIndex[msg.sender]; // 対象indexがなければ終了 require(index != 0); // 現在、ペンディング中のoperation情報を検索 var pending = pendings[_operation]; // そのオペレーションがまだ一度も実行されていないときは値を初期化 if (pending.yetNeeded == 0) { // 必要な実行数を初期化 pending.yetNeeded = required; // 確認済みオーナー数を0リセット pending.ownersDone = 0; // indexをセット pending.index = pendingsIndex.length++; pendingsIndex[pending.index] = _operation; } // オーナーのビットを決定 uint256 ownerIndexBit = 2**index; // オーナーが既に実行していないかのチェック(論理積) if (pending.ownersDone & ownerIndexBit == 0) { Confirmation(msg.sender, _operation); // この実行で承認数を満たす場合 if (pending.yetNeeded <= 1) { // 保留中の操作情報を削除(初期化)して終了 delete pendingsIndex[pendings[_operation].index]; delete pendings[_operation]; return true; } else { // 必要数に足りない場合 // 必要数を減算 pending.yetNeeded--; // 承認フラグにbitを立てる pending.ownersDone |= ownerIndexBit; } } return false; }
- このContractのキーとなる関数です。各オーナーが対象の操作に対して承認(実行)を行ったかを管理する関数しています。
pendings
は、実行を保留されている操作を管理する配列フィールドです。mapping(bytes32 => PendingState) pendings;
PendingState
という構造対を操作のバイト列をキーとして持ちます。
struct PendingState { uint256 yetNeeded; // その操作に必要な残りの実行オーナー数 uint256 ownersDone; // 現在実行したオーナー数 uint256 index; // 保留中の操作のindex }
PendingState
は操作の各状態をプロパティとして持っています。pendingsIndex
は、保留中の操作のindexを逆引きするための配列フィールドです。
revoke
function revoke(bytes32 _operation) external { uint256 index = ownerIndex[msg.sender]; if (index == 0) { return; } // 自分のビットを立てる uint256 ownerIndexBit = 2**index; var pending = pendings[_operation]; // 自分が承認(実行)しているか確認 if (pending.ownersDone & ownerIndexBit > 0) { pending.yetNeeded++; // 必要数の加算 pending.ownersDone -= ownerIndexBit; // ビットを削除 Revoke(msg.sender, _operation); } }
- 確認操作を取り消すための関数です。
onlyOwner modifier
modifier onlyOwner { require(isOwner(msg.sender)); _; }
- オーナーであれば実行できる権限を付与するためのmodifierです。
isOwner
function isOwner(address _addr) constant returns (bool) { return ownerIndex[_addr] > 0; }
- 自分がオーナーに登録されているかチェックする関数です。
hasConfirmed
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { var pending = pendings[_operation]; uint256 index = ownerIndex[_owner]; // オーナーであるかのチェック if (index == 0) { return false; } uint256 ownerIndexBit = 2**index; // ビット判定 // pending.ownersDone & ownerIndexBit > 0 でも良さそう? return !(pending.ownersDone & ownerIndexBit == 0); }
- 指定のオーナーが指定の操作に対して承認(実行)をチェックする関数です。
clearPending
function clearPending() internal { uint256 length = pendingsIndex.length; for (uint256 i = 0; i < length; ++i) { if (pendingsIndex[i] != 0) { delete pendings[pendingsIndex[i]]; } } delete pendingsIndex; }
- 保留中操作の初期化処理。全ての保留中操作をリセットする関数です。
- internalのためContract内部または、継承するContractからのみ実行を許可します。
感想
- ビット演算を使ってシンプルな仕組みで承認を管理する仕組みを実現しています。
- Ethereum上のプログラムは、チェーンサイズが限られていることや、Contractサイズが手数料に影響することもあり、バイトサイズを意識した実装は勉強になります。
- オーナーの数とビットを連動しているため、現状の実装だと255人が最大のようなので、大人数の承認システムを作りたい場合は別の方法を検討する必要がありそうです。