Safe(旧Gnosis Safe)を利用したDApps開発について


マルチシグのウォレットサービスとして定評のある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が提供してるサービスの紹介
![Screenshot 2023-10-29 at 16.40.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/20646202-319a-30ec-95ef-4b7b8ace3367.png)
#### Safe Core
開発者向けのサービス。SafeやSafeに互換性のあるアプリケーションとスムーズに連携して開発するためのSDKやAPIを提供。
#### Safe Wallet
一般ユーザー向けのサービス。マルチシグのウォレットコントラクトとUIを提供。

### Safe Coreについて
Safe Coreは大きく分けて３つのプロダクトを提供しています。
![Screenshot 2023-10-29 at 16.52.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/3f785e1b-2fd8-9002-a45a-ad13b098d95d.png)

#### 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
![Screenshot 2023-10-29 at 18.57.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/62603f78-f617-0797-80dd-578f19ef8247.png)

#### 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を確認してください。環境変数などローカルの設定を変更する必要がありますが、スクショのような結果が返ってくれば成功です。

https://github.com/safe-global/safe-core-sdk/tree/main/playground

### ① Safeコントラクトのデプロイ
```terminal
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

実行結果
![Screenshot 2023-10-30 at 4.47.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/bec6c382-03e3-009b-b783-565bbbaa1109.png)

### ② 提案の作成
```terminal
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()
```
https://github.com/safe-global/safe-core-sdk/blob/main/playground/api-kit/propose-transaction.ts

![Screenshot 2023-10-30 at 4.47.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/f11c2156-1c56-95f2-29b9-cb7c59811a88.png)


### ③ 提案の承認
必要な承認署名の数を２以上に設定した場合ここに変更を反映。
```terminal
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()
```

https://github.com/safe-global/safe-core-sdk/blob/main/playground/api-kit/confirm-transaction.ts

![Screenshot 2023-10-30 at 4.50.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/994acef5-97e7-2276-7c97-5dbeed9b6569.png)

![Screenshot 2023-10-30 at 4.50.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/6d93415f-405c-0740-37f3-7bdb07745d45.png)


### ④ トランザクションの実行
```terminal
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()
```
https://github.com/safe-global/safe-core-sdk/blob/main/playground/api-kit/execute-transaction.ts

![Screenshot 2023-10-30 at 4.49.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/43d366fe-4787-3842-3e86-d95d6338d31f.png)

#### 気になった点：
- SDKが強力な故に、Hardhatを使った操作に比べて処理がブラックボックス化してしまう。
- シームレスなUXはとても良いがオフチェーンでの署名管理などトラストレスではない処理も多い。
ガスレスな署名について→https://help.safe.global/en/articles/40865-gas-less-signatures

## 最後に：
クレカ決済、メタトランザクション、アカウントアブストラクションなどの機能がSafeエコシステムに統合されているのは、とても便利だと思いました。是非皆さんもDAppsの開発にSafeを利用してみてください。