Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dapper Contract 解説/Dapper Contract

Ara
August 26, 2019

Dapper Contract 解説/Dapper Contract

Dapper Contract 解説
2019/08/26 @avcdsld

基礎編

Ethereum のアドレスは 2 種類ある
EOA (Externally Owned Account) 秘密鍵で署名して Tx を送れる
コントラクトアカウント (Contract account) 秘密鍵がなく署名できない EOA 起点で、インターナル Tx を送れる

コントラクトウォレットとは
コントラクトアカウントをユーザーのウォレットとして使っているもの
現在リリースされているプロダクト:
Dapper https://www.meetdapper.com/
Argent https://www.argent.xyz/

Dapper

Argent

Dapper 概説

Dapper の概要
CryptoKitties 開発チームが開発
MetaMask ライクなブラウザ拡張
Gas 代を Dapper チーム側で持つことができる
リカバリー機能がある
オープンソース https://github.com/dapperlabs/dapper-contracts

要点
2 of 2 のマルチシグウォレットになっている
Device Key ユーザーがデバイスに秘密鍵を保管
Cosigning Key Dapper チームがサーバーに秘密鍵を保管
リカバリー用の Key がある
Recovery Key Dapperチームがコールドウォレットに保管
Backup Key リカバリー時に、唯一の Device Key になる ユーザーが任意の場所に秘密鍵を保管
(たぶん)
※ コードでは authorizedAddress / signer
※ コードでは cosigner

構図
ユーザー
Dapperチーム
Contract
Tx送信
コールド
ウォレット
Recovery Key
Device
Key
Cosigning
Key
transfer などの操作を実行
署名

構図
ユーザー
Dapperチーム
Contract
Tx送信
Device
Key
Cosigning
Key
・既存の Key を無効化・新しい Key を登録
リカバリー時
コールド
ウォレット
Recovery Key

コントラクトの呼び出しフローと
いくつかの例

コントラクトの呼び出しフロー
コントラクト生成 ― コントラクトからコントラクトを生成
初期化 ― Device Key, Cosigning Key, Recovery Key を設定
利用 ― 送金などの操作内容の Tx をコントラクトに送信
invoke0(): Device Key = Cosigning Key の Tx 送信
invoke1SignerSend(): Cosigning Key の署名 + Device Key の Tx 送信
invoke1CosignerSend(): Device Key の署名 + Cosigning Key の Tx 送信
invoke2(): Device Key の署名 + Cosigning Key の署名 + 任意者による Tx 送信
リカバリー ― 既存の Key を無効化して、Backup Key を唯一の Device Key   として登録する Tx を Recovery Key で送信

ETH の受け取り
fallback関数
・Received イベント発行
Dapper Contract
ユーザー
3 ETH

ETH の送金
ユーザー
Dapper
invoke1CosignerSend()
・署名/Nonce/送信者のチェック
Dapper Contract
internalInvoke()
・送金先アドレスに、コントラクト内の ETH を送金
送金先ユーザー
ユーザーの署名
Nonce
Data
送信先アドレス
送金額 (3 ETH)
InnerData (空)
3 ETH
* invoke1CosignerSend でなく, invoke1SignerSend, invoke2, invoke0(Device Key = Cosiging Key の場合)を使うこともできる

ERC-721 トークンの受け取り
transferFrom()
・トークンの所有者変更
ERC-721 Token Contract
ユーザー
送信先のコントラクトウォレットのアドレス
TokenId

ERC-721 トークンの受け取り
safeTransferFrom()
・onERC721Received チェック
・トークンの所有者変更
ERC-721 Token Contract
ユーザー
送信先のコントラクトウォレットのアドレス
TokenId
onERC721Received()
Dapper Contract
safeTransferFrom を使う場合

ERC-721 トークンの送信
ユーザー
Dapper
invoke1CosignerSend()
・署名/Nonce/送信者のチェック
Dapper Contract
internalInvoke()
・ERC-721 トークンのコントラクト アドレスの指定された関数を InnerDate で呼び出す
ユーザーの署名
Nonce
Data
ERC-721 トークンのコントラクトアドレス
送金額 (0 ETH)
InnerData
関数シグネチャ
送信先アドレス
TokenId
* invoke1CosignerSend でなく, invoke1SignerSend, invoke2, invoke0(Device Key = Cosiging Key の場合)を使うこともできる
transferFrom()
・トークンの所有者変更
ERC-721 Token Contract

Dapper コントラクトのここがすごい

Dapperコントラクトのここがすごい
技術面
コントラクト生成が効率的
マルチシグ/メタトランザクションの実践的な実装
ユーザービリティ面
サービス提供者が Gas 代を肩代わりできる
盗難リスクを抑えたリカバリー機能の提供
コントラクトにユーザーが機能を追加できる

コントラクト生成が効率的
EIP-1167: Minimal Proxy Contract を利用
生成するコントラクトは たった 45 バイト(Gas ~100K)
指定したアドレスのコントラクトに DelegateCall
※フルコードを生成する関数も用意はされている

コントラクト生成が効率的
EIP 1167 のバイトコード( bebebe... の箇所が任意のコントラクトアドレス)
Etherscan でのデコンパイル結果

コントラクト生成が効率的
Etherscan でみると、Minimal Proxy Contract として表示されている(少し前までこの表示はなかった. 利用例が増えてきたのかも)

マルチシグ/メタトランザクションの実践的な実装
Ethereum は標準でマルチシグ機能を持っていない
セキュアなマルチシグ/メタトランザクションを書くのは難しい
コード監査されていて、ユーザーも多いので実装例として非常に価値がある

サービス提供者が Gas 代を肩代わりできる
ユーザー
サービス提供者
Contract
Tx送信
サービス提供者がGas 代を負担
署名
ユーザーが ETH を持っていなくてもサービスを利用できる
ETH を使わないサービスは特に UX 向上が見込める

サービス提供者が Gas 代を肩代わりできる
ユーザー
サービス提供者
Contract
Tx送信
署名
ちなみに、 ユーザーに Gas 代を払わせることもできる

盗難リスクを抑えたリカバリー機能の提供
通常のウォレットでは…
ユーザーが秘密鍵をなくしたらリカバリー不可能
…かといって、サービス提供側が秘密鍵を持つと、ハッキングによる盗難リスクが高まる
Dapper コントラクトウォレットでは…
ウォレットがコントラクトなので、秘密鍵はなく、独自のリカバリー機能を提供できている
サービス提供者の内部犯行がない前提なら、Recovery Key をコールドウォレットで管理できるため盗難リスクを抑えたリカバリー機能を実現できる

盗難リスクを抑えたリカバリー機能の提供
/// @notice Performs an emergency recovery operation, removing all existing authorizations and setting
/// a sole new authorized address with optional cosigner. THIS IS A SCORCHED EARTH SOLUTION, and great
/// care should be taken to ensure that this method is never called unless it is a last resort. See the
/// comments above about the proper kinds of addresses to use as the recoveryAddress to ensure this method
/// is not trivially abused.
/// @param _authorizedAddress the new and sole authorized address
/// @param _cosigner the corresponding cosigner address, can be equal to _authorizedAddress
function emergencyRecovery(address _authorizedAddress, uint256 _cosigner) external onlyRecoveryAddress {
require(_authorizedAddress != address(0), "Authorized addresses must not be zero.");
require(_authorizedAddress != recoveryAddress, "Do not use the recovery address as an authorized address.");
require(address(_cosigner) != address(0), "The cosigner must not be zero.");

// Incrementing the authVersion number effectively erases the authorizations mapping. See the comments
// on the authorizations variable (above) for more information.
authVersion += AUTH_VERSION_INCREMENTOR;

// Store the new signer/cosigner pair as the only remaining authorized address
authorizations[authVersion + uint256(_authorizedAddress)] = _cosigner;
emit EmergencyRecovery(_authorizedAddress, _cosigner);
}
リカバリー用の関数
https://github.com/dapperlabs/dapper-contracts/blob/0321efcc80c413dc1d3a3cd30ae02929b3ddadfe/contracts/Wallet/CoreWallet.sol#L299

コントラクトにユーザーが機能を追加できる
任意の関数とコントラクトアドレスを登録できる
例えば 新しいトークン規格が出てきても、同じコントラクトで対応できる(サポートしているインターフェースを返せる)
https://github.com/dapperlabs/dapper-contracts/blob/0321efcc80c413dc1d3a3cd30ae02929b3ddadfe/contracts/Wallet/CoreWallet.sol#L261
function setDelegate(bytes4 _interfaceId, address _delegate) external onlyInvoked {
delegates[_interfaceId] = _delegate;
emit DelegateUpdated(_interfaceId, _delegate);
}
登録用の関数
※ EIP-165 準拠

コントラクトにユーザーが機能を追加できる
function() external payable {
:
if (msg.data.length > 0) {
address delegate = delegates[msg.sig];
:
assembly {
calldatacopy(0, 0, calldatasize())
let result := staticcall(gas, delegate, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
:
}
}
}
https://github.com/dapperlabs/dapper-contracts/blob/0321efcc80c413dc1d3a3cd30ae02929b3ddadfe/contracts/Wallet/CoreWallet.sol#L221

※ fallback 関数(無名の関数)は、コントラクトの関数呼び出しで関数シグネチャが見つからない場合に呼ばれる
fallback 関数に、追加した機能を呼び出すしくみが入っている

コントラクトにユーザーが機能を追加できる
追加した機能をサポートインターフェースとして返すしくみ
https://github.com/dapperlabs/dapper-contracts/blob/0321efcc80c413dc1d3a3cd30ae02929b3ddadfe/contracts/Wallet/CoreWallet.sol#L425
function supportsInterface(bytes4 interfaceID) external view returns (bool) {
// First check if the ID matches one of the interfaces we support statically.
if (
interfaceID == this.supportsInterface.selector || // ERC165
interfaceID == ERC721_RECEIVED_FINAL || // ERC721 Final
interfaceID == ERC721_RECEIVED_DRAFT || // ERC721 Draft
interfaceID == ERC223_ID || // ERC223
interfaceID == ERC1271_VALIDSIGNATURE // ERC1271
) {
return true;
}
// If we don't support the interface statically, check whether we have added
// dynamic support for it.
return uint256(delegates[interfaceID]) > 0;
}

その他開発関連・疑問点

Dapper ブラウザ拡張とのインテグレーション
要件:Node v9.x.x, web3.js v1.x.x
Dapper Sandbox https://www.meetdapper.com/docs/start
Rinkeby で動作する開発者向けのブラウザ拡張. Gas 代を Dapper チームが払ってくれる
通常の Web3 プロバイダーと同じように Dapper に接続できる
EIP-1102 (eth_requestAccounts) にも対応
// Transactions will be signed and sent via your Dapper account.
const web3 = new Web3(window.ethereum);
const Contract = new web3.eth.Contract(contractJSON.abi, <コントラクトアドレス>);
const accounts = await window.ethereum.enable();

// Use Infura provider to listen for events from your contract!
const newestWeb3 = new Web3(new Web3.providers.WebsocketProvider(
"wss://rinkeby.infura.io/ws/v3/"));
newestWeb3.eth.defaultAccount = accounts[0];
const eventSource = new newestWeb3.eth.Contract(contractJSON.abi, <コントラクトアドレス>);

return { web3, accounts, Contract, eventSource }

疑問点
cosigner が address 型でなく uint256 型なのはなぜ?→ assembly でデータを処理するとき、mload だと 32 byte でしか読めないので、12 byte 分 左シフトしている. そのため、12 byte のダミーデータをつけられるように uint256 型にしている
Dapper ブラウザ拡張と MetaMask との違いはあるか?→ 開発者向けドキュメントによると、同じようにインテグレーションできる
関数シグネチャが同じで、異なるコントラクトの呼び出しを使い分けることはできない?→ できないはず
コントラクトにユーザーが機能を追加できるが、例えばどんなユースケースがあるか? → (いいユースケースがあれば知りたい)

参考
https://github.com/dapperlabs/dapper-contracts
https://etherscan.io/address/0x37932f3eca864632156ccba7e2814b51a374caec#code
https://medium.com/dapperlabs/why-dapper-is-a-smart-contract-wallet-ef44cc51cfa5
https://medium.com/dapperlabs/introducing-dappers-multi-device-support-db6b4f53fb
https://blog.sigmaprime.io/dapper-wallet-review.html

※間違っている点があればご指摘いただけると助かります。

Ara

August 26, 2019
Tweet

More Decks by Ara

Other Decks in Programming

Transcript

  1. Ethereum のアドレスは 2 種類ある • EOA (Externally Owned Account) 秘密鍵で署名して

    Tx を送れる • コントラクトアカウント (Contract account) 秘密鍵がなく署名できない EOA 起点で、インターナル Tx を送れる
  2. Dapper の概要 • CryptoKitties 開発チームが開発 • MetaMask ライクなブラウザ拡張 • Gas

    代を Dapper チーム側で持つことができる • リカバリー機能がある • オープンソース https://github.com/dapperlabs/dapper-contracts
  3. 要点 • 2 of 2 のマルチシグウォレットになっている ◦ Device Key ユーザーがデバイスに秘密鍵を保管

    ◦ Cosigning Key Dapper チームがサーバーに秘密鍵を保管 • リカバリー用の Key がある ◦ Recovery Key Dapperチームがコールドウォレットに保管 ◦ Backup Key リカバリー時に、唯一の Device Key になる ユーザーが任意の場所に秘密鍵を保管 (たぶん) ※ コードでは authorizedAddress / signer ※ コードでは cosigner
  4. 構図 ユーザー Dapperチーム Contract Tx送信 Device Key Cosigning Key ・既存の

    Key を無効化 ・新しい Key を登録 リカバリー時 コールド ウォレット Recovery Key
  5. コントラクトの呼び出しフロー 1. コントラクト生成 ― コントラクトからコントラクトを生成 2. 初期化 ― Device Key,

    Cosigning Key, Recovery Key を設定 3. 利用 ― 送金などの操作内容の Tx をコントラクトに送信 ◦ invoke0(): Device Key = Cosigning Key の Tx 送信 ◦ invoke1SignerSend(): Cosigning Key の署名 + Device Key の Tx 送信 ◦ invoke1CosignerSend(): Device Key の署名 + Cosigning Key の Tx 送信 ◦ invoke2(): Device Key の署名 + Cosigning Key の署名 + 任意者による Tx 送信 4. リカバリー ― 既存の Key を無効化して、Backup Key を唯一の Device Key   として登録する Tx を Recovery Key で送信
  6. ETH の送金 ユーザー Dapper invoke1CosignerSend() ・署名/Nonce/送信者のチェック Dapper Contract internalInvoke() ・送金先アドレスに、コントラクト内の

     ETH を送金 送金先ユーザー • ユーザーの署名 • Nonce • Data ◦ 送信先アドレス ◦ 送金額 (3 ETH) ◦ InnerData (空) 3 ETH * invoke1CosignerSend でなく, invoke1SignerSend, invoke2, invoke0(Device Key = Cosiging Key の場合)を使うこともできる
  7. ERC-721 トークンの受け取り safeTransferFrom() ・onERC721Received チェック ・トークンの所有者変更 ERC-721 Token Contract ユーザー

    • 送信先のコントラクトウォレットのアドレス • TokenId onERC721Received() Dapper Contract safeTransferFrom を使う場合
  8. ERC-721 トークンの送信 ユーザー Dapper invoke1CosignerSend() ・署名/Nonce/送信者のチェック Dapper Contract internalInvoke() ・ERC-721

    トークンのコントラクト  アドレスの指定された関数を  InnerDate で呼び出す • ユーザーの署名 • Nonce • Data ◦ ERC-721 トークンの コントラクトアドレス ◦ 送金額 (0 ETH) ◦ InnerData ▪ 関数シグネチャ ▪ 送信先アドレス ▪ TokenId * invoke1CosignerSend でなく, invoke1SignerSend, invoke2, invoke0(Device Key = Cosiging Key の場合)を使うこともできる transferFrom() ・トークンの所有者変更 ERC-721 Token Contract
  9. Dapperコントラクトのここがすごい 技術面 • コントラクト生成が効率的 • マルチシグ/メタトランザクションの実践的な実装 ユーザービリティ面 • サービス提供者が Gas

    代を肩代わりできる • 盗難リスクを抑えたリカバリー機能の提供 • コントラクトにユーザーが機能を追加できる
  10. コントラクト生成が効率的 • EIP-1167: Minimal Proxy Contract を利用 • 生成するコントラクトは たった

    45 バイト(Gas ~100K) • 指定したアドレスのコントラクトに DelegateCall ※フルコードを生成する関数も用意はされている
  11. サービス提供者が Gas 代を肩代わりできる ユーザー サービス提供者 Contract Tx送信 サービス提供者が Gas 代を負担

    署名 • ユーザーが ETH を持っていなくてもサービスを利用できる • ETH を使わないサービスは特に UX 向上が見込める
  12. 盗難リスクを抑えたリカバリー機能の提供 通常のウォレットでは… • ユーザーが秘密鍵をなくしたらリカバリー不可能 • …かといって、サービス提供側が秘密鍵を持つと、 ハッキングによる盗難リスクが高まる Dapper コントラクトウォレットでは… •

    ウォレットがコントラクトなので、秘密鍵はなく、 独自のリカバリー機能を提供できている • サービス提供者の内部犯行がない前提なら、 Recovery Key をコールドウォレットで管理できるため 盗難リスクを抑えたリカバリー機能を実現できる
  13. 盗難リスクを抑えたリカバリー機能の提供 /// @notice Performs an emergency recovery operation, removing all

    existing authorizations and setting /// a sole new authorized address with optional cosigner. THIS IS A SCORCHED EARTH SOLUTION, and great /// care should be taken to ensure that this method is never called unless it is a last resort. See the /// comments above about the proper kinds of addresses to use as the recoveryAddress to ensure this method /// is not trivially abused. /// @param _authorizedAddress the new and sole authorized address /// @param _cosigner the corresponding cosigner address, can be equal to _authorizedAddress function emergencyRecovery(address _authorizedAddress, uint256 _cosigner) external onlyRecoveryAddress { require(_authorizedAddress != address(0), "Authorized addresses must not be zero."); require(_authorizedAddress != recoveryAddress, "Do not use the recovery address as an authorized address."); require(address(_cosigner) != address(0), "The cosigner must not be zero."); // Incrementing the authVersion number effectively erases the authorizations mapping. See the comments // on the authorizations variable (above) for more information. authVersion += AUTH_VERSION_INCREMENTOR; // Store the new signer/cosigner pair as the only remaining authorized address authorizations[authVersion + uint256(_authorizedAddress)] = _cosigner; emit EmergencyRecovery(_authorizedAddress, _cosigner); } リカバリー用の関数 https://github.com/dapperlabs/dapper-contracts/blob/0321efcc80c413dc1d3a3cd30ae02929b3ddadfe/contracts/Wallet/CoreWallet.sol#L299
  14. コントラクトにユーザーが機能を追加できる function() external payable { : if (msg.data.length > 0)

    { address delegate = delegates[msg.sig]; : assembly { calldatacopy(0, 0, calldatasize()) let result := staticcall(gas, delegate, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) : } } } https://github.com/dapperlabs/dapper-contracts/blob/0321efcc80c413dc1d3a3cd30ae02929b3ddadfe/contracts/Wallet/CoreWallet.sol#L221 ※ fallback 関数(無名の関数)は、コントラクトの関数呼び出しで関数シグネチャが見つからない場合に呼ばれる • fallback 関数に、追加した機能を呼び出すしくみが入っている
  15. コントラクトにユーザーが機能を追加できる • 追加した機能をサポートインターフェースとして返すしくみ https://github.com/dapperlabs/dapper-contracts/blob/0321efcc80c413dc1d3a3cd30ae02929b3ddadfe/contracts/Wallet/CoreWallet.sol#L425 function supportsInterface(bytes4 interfaceID) external view returns

    (bool) { // First check if the ID matches one of the interfaces we support statically. if ( interfaceID == this.supportsInterface.selector || // ERC165 interfaceID == ERC721_RECEIVED_FINAL || // ERC721 Final interfaceID == ERC721_RECEIVED_DRAFT || // ERC721 Draft interfaceID == ERC223_ID || // ERC223 interfaceID == ERC1271_VALIDSIGNATURE // ERC1271 ) { return true; } // If we don't support the interface statically, check whether we have added // dynamic support for it. return uint256(delegates[interfaceID]) > 0; }
  16. Dapper ブラウザ拡張とのインテグレーション • 要件:Node v9.x.x, web3.js v1.x.x • Dapper Sandbox

    https://www.meetdapper.com/docs/start ◦ Rinkeby で動作する開発者向けのブラウザ拡張. Gas 代を Dapper チームが払ってくれる • 通常の Web3 プロバイダーと同じように Dapper に接続できる • EIP-1102 (eth_requestAccounts) にも対応 // Transactions will be signed and sent via your Dapper account. const web3 = new Web3(window.ethereum); const Contract = new web3.eth.Contract(contractJSON.abi, <コントラクトアドレス>); const accounts = await window.ethereum.enable(); // Use Infura provider to listen for events from your contract! const newestWeb3 = new Web3(new Web3.providers.WebsocketProvider( "wss://rinkeby.infura.io/ws/v3/<INFURA_PROJECT_ID>")); newestWeb3.eth.defaultAccount = accounts[0]; const eventSource = new newestWeb3.eth.Contract(contractJSON.abi, <コントラクトアドレス>); return { web3, accounts, Contract, eventSource }
  17. 疑問点 • cosigner が address 型でなく uint256 型なのはなぜ? → assembly

    でデータを処理するとき、mload だと 32 byte でしか読めな いので、12 byte 分 左シフトしている. そのため、12 byte のダミーデータ をつけられるように uint256 型にしている • Dapper ブラウザ拡張と MetaMask との違いはあるか? → 開発者向けドキュメントによると、同じようにインテグレーションできる • 関数シグネチャが同じで、異なるコントラクトの呼び出しを使い分けること はできない?→ できないはず • コントラクトにユーザーが機能を追加できるが、例えばどんなユースケース があるか? → (いいユースケースがあれば知りたい)