マルチシグのウォレットサービスとして定評のあるSafe(旧Gnosis Safe)が色々進化していると聞いたので調査しました。WEB3関連のハッカソンでも導入している開発チームが増えてきているようです。
website:https://safe.global/
document:https://docs.safe.global/getting-started/readme
記事の内容
前半:Safeが提供してるサービスの紹介
後半:SDKを使った基本操作のデモ
UIを使ったSafeの操作を体験済みの方は多いと思いますが、今回のデモではスクリプトからの操作に挑戦します。Safeコントラクトのデプロイから、トランザクションの作成までをSDKを使ったスクリプトで実行します。
目標
Safeが提供するSDKを使って、Vitalik氏にGoeril etherを1weiプレゼント。
Safeをスクリプトから実行できると、何が嬉しいのか?
- システムに気軽にマルチシグを組み込めるようになる
- コントラクトの操作やトランザクション実行などをより安全に実行できるようになる
- AA(アカウントアブストラクション)やメタトランザクションをSafeのSDKを使ってサービスに導入できる
記事で説明しないこと
- AAやメタトランザクションなどの技術的な詳細
- 連携する外部サービスの詳細の仕組み
Safeが提供してるサービスの紹介
Safe Core
開発者向けのサービス。SafeやSafeに互換性のあるアプリケーションとスムーズに連携して開発するためのSDKやAPIを提供。
Safe Wallet
一般ユーザー向けのサービス。マルチシグのウォレットコントラクトとUIを提供。
Safe Coreについて
Safe Coreは大きく分けて3つのプロダクトを提供しています。
Safe{Core} Account Abstraction SDK
Safe{Core} Account Abstraction SDKは、Safeを異なる外部サービスプロバイダーと統合するのを助ける開発者キットのセットです。
Safe{Core} API
Safe{Core} APIは、Safeアカウントに関連する情報を操作するためのAPIです。これには、Safe Transaction Service、Safe Events Serviceなどが含まれます。
Safe{Core} Protocol
Safe{Core} Protocolは、スマートアカウントを安全かつ、コンポーザブルにするためのオープンなフレームワークです。コントラクトの制御をカスタマイズすることが可能です。
Safe{Core} Account Abstraction SDK
Protocol Kit
Protocol Kitは、Safeのコントラクトとのやりとりを簡単にしてくれます。これには、新しいSafeアカウントの作成、設定の更新、取引の署名と実行などが含まれます。
Auth Kit
アカウントアブストラクションの技術を応用してソーシャルログインなどを可能にします。
Web3Authと連携した機能です。
Onramp Kit
Onramp Kitは、ユーザーが法定通貨で暗号通貨を購入し、クレジットカードやその他の支払い方法を通じてSafeアカウントに資金を提供するのを助けます。
Stripeと連携した機能です。
Relay Kit
Relay KitはSafeの取引を中継し、第三者によるスポンサーを受けたり、サポートされている任意のERC-20トークンで支払いを行ったりすることを可能にします。
Gelatoと連携した機能です。
API Kit
API KitはSafe Transaction Service APIとのやりとりを支援します。これには、署名者間で取引を共有し、Safeアカウントから情報を取得することが含まれます。たとえば、設定や取引履歴などです。
DApps開発で重要な観点
コントラクトとの疎通
- ユーザーのウォレット接続
- RPCノードとの疎通
セキュリティ
- 脆弱性の回避
- 適切な権限管理
- 安全な秘密鍵の管理
- ソーシャルエンジニアリングによるハッキング防止
ユーザーリテラシに応じたUX
- AAを利用したウォレットを意識しないUX
- メタトランザクションによるガスレスなトランザクション処理
- 法定通貨やERC20などネイティブトークン以外を利用した決済手段
...などなど
Safeを利用した対応
コントラクトとの疎通
- ユーザーのウォレット接続
→ Safe{Core} Protocol kit - RPCノードとの疎通
→ Safe{Core} Protocol kit
セキュリティ
- 脆弱性の回避
→ Safe Wallet - 適切な権限管理
→ Safe Wallet - 安全な秘密鍵の管理
→ Safe Wallet
ユーザーリテラシに応じたUX
- AAを利用したウォレットを意識しないUX
→ Safe{Core} Auth Kit - メタトランザクションによるガスレスなトランザクション処理
→ Safe{Core} Relay Kit - 法定通貨やERC20などネイティブトークン以外を利用した決済手段
→ Safe{Core} Relay Kit/OnRamp Kit
...などなど
Protocol KitとAPI Kitを使った簡易的なデモ
スクリプトを実行して、実際にマルチシグのウォレットからETHを送信してみましょう。
Protocol KitのリポジトリのPlaygroundディレクトリで紹介されている内容と同じです。
① Deploy:Safeコントラクトのデプロイ
↓
② Propose:トランザクションの提案
↓
③ Confirm or Reject:提案に対しての承認、もしくは拒否
↓
④ Execute:トランザクションの実行
SDKを使った基本操作のデモ
Safeが提供するSDKのリポジトリには、マルチシグウォレットのコントラクトのデプロイからトランザクションを実行するまでのサンプルコードがあります。詳細に関しては、PlaygroundディレクトリのReadmeを確認してください。環境変数などローカルの設定を変更する必要がありますが、スクショのような結果が返ってくれば成功です。
① Safeコントラクトのデプロイ
yarn play deploy-safe
メモと検証用の変更が反映されたスクリプトです。参考までに。
import { SafeAccountConfig, SafeFactory } from '@safe-global/protocol-kit'
import { EthersAdapter } from '@safe-global/protocol-kit'
import { ethers } from 'ethers'
// This file can be used to play around with the Safe Core SDK
interface Config {
RPC_URL: string
DEPLOYER_ADDRESS_PRIVATE_KEY: string
DEPLOY_SAFE: {
OWNERS: string[]
THRESHOLD: number
SALT_NONCE: string
}
}
// 必要な環境変数を設定
const config: Config = {
RPC_URL: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
DEPLOYER_ADDRESS_PRIVATE_KEY: `${process.env.OWNER_1_PRIVATE_KEY!}`,
DEPLOY_SAFE: {
OWNERS: [
'0xCb96dAb95293D721283e1550aE49485BFD56dd12',
'0x4b54a665F366fF519e0c84549E105fB6b5EFa8aa'
],
THRESHOLD: 2, // <SAFE_THRESHOLD>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
SALT_NONCE: process.env.SALT_NONCE!
}
}
async function main() {
const provider = new ethers.providers.JsonRpcProvider(config.RPC_URL)
const deployerSigner = new ethers.Wallet(config.DEPLOYER_ADDRESS_PRIVATE_KEY, provider)
// 各インスタンスの初期化
const ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: deployerSigner
})
const safeFactory = await SafeFactory.create({ ethAdapter })
const safeAccountConfig: SafeAccountConfig = {
owners: config.DEPLOY_SAFE.OWNERS,
// ここでトランザクションの実行に必要な署名の数を設定
threshold: config.DEPLOY_SAFE.THRESHOLD
}
const saltNonce = config.DEPLOY_SAFE.SALT_NONCE
const predictedDeploySafeAddress = await safeFactory.predictSafeAddress(
safeAccountConfig,
saltNonce
)
console.log('Predicted deployed Safe address:', predictedDeploySafeAddress)
function callback(txHash: string) {
console.log('Transaction hash:', txHash)
}
// Safeコントラクトのデプロイ
const safe = await safeFactory.deploySafe({
safeAccountConfig,
saltNonce,
callback
})
console.log('Deployed Safe:', safe.getAddress())
}
main()
リポジトリの対象箇所
https://github.com/safe-global/safe-core-sdk/blob/main/playground/protocol-kit/deploy-safe.ts
② 提案の作成
yarn play propose-transaction
import SafeApiKit from '@safe-global/api-kit'
import Safe, { EthersAdapter } from '@safe-global/protocol-kit'
import { OperationType, SafeTransactionDataPartial } from '@safe-global/safe-core-sdk-types'
import { ethers } from 'ethers'
interface Config {
RPC_URL: string
SIGNER_ADDRESS_PRIVATE_KEY: string
SAFE_ADDRESS: string
TX_SERVICE_URL: string
}
const config: Config = {
RPC_URL: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
SIGNER_ADDRESS_PRIVATE_KEY: `${process.env.OWNER_1_PRIVATE_KEY!}`,
SAFE_ADDRESS: `${process.env.SAFE_ADDRESS}`,
TX_SERVICE_URL: 'https://safe-transaction-goerli.safe.global/'
}
async function main() {
const provider = new ethers.providers.JsonRpcProvider(config.RPC_URL)
const signer = new ethers.Wallet(config.SIGNER_ADDRESS_PRIVATE_KEY, provider)
const ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: signer
})
const safe = await Safe.create({
ethAdapter,
safeAddress: config.SAFE_ADDRESS
})
const service = new SafeApiKit({
txServiceUrl: config.TX_SERVICE_URL,
ethAdapter
})
// トランザクションの作成
const safeTransactionData: SafeTransactionDataPartial = {
// vitalik氏に日頃の感謝の気持ちを込めてGoeril etherを1weiをプレゼント
to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
value: '1', // 1 wei
data: '0x',
operation: OperationType.Call
}
const safeTransaction = await safe.createTransaction({ safeTransactionData })
const senderAddress = await signer.getAddress()
const safeTxHash = await safe.getTransactionHash(safeTransaction)
const signature = await safe.signTransactionHash(safeTxHash)
// プロボーザルの作成
await service.proposeTransaction({
safeAddress: config.SAFE_ADDRESS,
safeTransactionData: safeTransaction.data,
safeTxHash,
senderAddress,
senderSignature: signature.data
})
console.log('Proposed a transaction with Safe:', config.SAFE_ADDRESS)
console.log('- safeTxHash:', safeTxHash)
console.log('- Sender:', senderAddress)
console.log('- Sender signature:', signature.data)
}
main()
③ 提案の承認
必要な承認署名の数を2以上に設定した場合ここに変更を反映。
yarn play confirm-transaction
import SafeApiKit from '@safe-global/api-kit'
import Safe, { EthersAdapter } from '@safe-global/protocol-kit'
import { ethers } from 'ethers'
// This file can be used to play around with the Safe Core SDK
interface Config {
RPC_URL: string
SIGNER_ADDRESS_PRIVATE_KEY: string
SAFE_ADDRESS: string
TX_SERVICE_URL: string
SAFE_TX_HASH: string
}
const config: Config = {
RPC_URL: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
SIGNER_ADDRESS_PRIVATE_KEY: `${process.env.OWNER_1_PRIVATE_KEY!}`,
SAFE_ADDRESS: `${process.env.SAFE_ADDRESS}`,
TX_SERVICE_URL: 'https://safe-transaction-goerli.safe.global/', // Check https://docs.safe.global/safe-core-api/available-services
SAFE_TX_HASH: process.env.SAFE_TX_HASH || 'testhash' // propose-transactionを実行した取得したトランザクションハッシュ
}
async function main() {
const provider = new ethers.providers.JsonRpcProvider(config.RPC_URL)
const signer = new ethers.Wallet(config.SIGNER_ADDRESS_PRIVATE_KEY, provider)
const ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: signer
})
const safe = await Safe.create({
ethAdapter,
safeAddress: config.SAFE_ADDRESS
})
const service = new SafeApiKit({
txServiceUrl: config.TX_SERVICE_URL,
ethAdapter
})
// トランザクションを取得
const transaction = await service.getTransaction(config.SAFE_TX_HASH)
// const pendingTransactions = await service.getPendingTransactions()
// const transactions = await service.getIncomingTransactions()
// const transactions = await service.getMultisigTransactions()
// const transactions = await service.getModuleTransactions()
// const transactions = await service.getAllTransactions()
console.log(transaction)
const safeTxHash = transaction.safeTxHash
const signature = await safe.signTransactionHash(safeTxHash)
// トランザクションの承認
const signatureResponse = await service.confirmTransaction(safeTxHash, signature.data)
const signerAddress = await signer.getAddress()
console.log('Added a new signature to transaction with safeTxGas:', config.SAFE_TX_HASH)
console.log('- Signer:', signerAddress)
console.log('- Signer signature:', signatureResponse.signature)
}
main()
④ トランザクションの実行
yarn play execute-transaction
import SafeApiKit from '@safe-global/api-kit'
import Safe, { EthersAdapter } from '@safe-global/protocol-kit'
import { ethers } from 'ethers'
// This file can be used to play around with the Safe Core SDK
interface Config {
RPC_URL: string
SIGNER_ADDRESS_PRIVATE_KEY: string
SAFE_ADDRESS: string
TX_SERVICE_URL: string
SAFE_TX_HASH: string
}
const config: Config = {
RPC_URL: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
SIGNER_ADDRESS_PRIVATE_KEY: `${process.env.OWNER_1_PRIVATE_KEY!}`,
SAFE_ADDRESS: `${process.env.SAFE_ADDRESS}`,
TX_SERVICE_URL: 'https://safe-transaction-goerli.safe.global/', // Check https://docs.safe.global/safe-core-api/available-services
SAFE_TX_HASH: process.env.SAFE_TX_HASH || 'testhash' // propose-transactionを実行した取得したトランザクションハッシュ
}
async function main() {
const provider = new ethers.providers.JsonRpcProvider(config.RPC_URL)
const signer = new ethers.Wallet(config.SIGNER_ADDRESS_PRIVATE_KEY, provider)
const ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: signer
})
const safe = await Safe.create({
ethAdapter,
safeAddress: config.SAFE_ADDRESS
})
const service = new SafeApiKit({
txServiceUrl: config.TX_SERVICE_URL,
ethAdapter
})
const safeTransaction = await service.getTransaction(config.SAFE_TX_HASH)
console.log('safeTransaction:', safeTransaction)
// トランザクションが実行可能か確認
const isTxExecutable = await safe.isValidTransaction(safeTransaction)
console.log('isTxExecutable:', isTxExecutable)
if (isTxExecutable) {
// トランザクションの実行
// Safeから送金する場合、SafeにEtherがないと実行不可の判定になるので注意
const txResponse = await safe.executeTransaction(safeTransaction)
const contractReceipt = await txResponse.transactionResponse?.wait()
console.log('Transaction executed.')
console.log('- Transaction hash:', contractReceipt?.transactionHash)
} else {
console.log('Transaction invalid. Transaction was not executed.')
}
}
main()
気になった点:
- SDKが強力な故に、Hardhatを使った操作に比べて処理がブラックボックス化してしまう。
- シームレスなUXはとても良いがオフチェーンでの署名管理などトラストレスではない処理も多い。
ガスレスな署名について→https://help.safe.global/en/articles/40865-gas-less-signatures
最後に:
クレカ決済、メタトランザクション、アカウントアブストラクションなどの機能がSafeエコシステムに統合されているのは、とても便利だと思いました。是非皆さんもDAppsの開発にSafeを利用してみてください。