📚

Viemの基礎学習2日目(スマートコントラクトとの対話)

に公開

ViemとFoundryを組み合わせたスマートコントラクト開発

日付: 2025年10月26日
学習内容: FoundryとViemを組み合わせたスマートコントラクトのデプロイとインタラクション、関数セレクター、ABIを使用した実践的な開発

1. プロジェクト構造の作成

1.1 プロジェクトの構成

viem/
├── foundry/              # Foundryプロジェクト
│   ├── src/
│   │   └── SomeContract.sol
│   ├── script/
│   │   └── Deploy.s.sol
│   └── foundry.toml
├── src/                  # TypeScriptコード
│   ├── deploy.ts
│   ├── contract-interaction.ts
│   ├── function-selector.ts
│   └── with-abi-file.ts
├── package.json
└── tsconfig.json

1.2 プロジェクトの初期化

# Foundryプロジェクトの初期化
forge init foundry --no-git

# Node.jsプロジェクトの初期化
npm init -y

# 必要なパッケージのインストール
npm install viem
npm install --save-dev tsx typescript @types/node

2. スマートコントラクトの作成

2.1 SomeContract.solの作成

Foundryプロジェクト内にシンプルなコントラクトを作成:

// foundry/src/SomeContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;

contract SomeContract {
    uint256 public myUint = 10;

    function setUint(uint256 _myUint) public {
        myUint = _myUint;
    }
}

ポイント:

  • myUint: public変数(自動的にgetter関数が生成される)
  • setUint(): 値を更新する関数
  • 初期値は10に設定

2.2 コンパイル

cd foundry
forge build
cd ..

コンパイル後、foundry/out/SomeContract.sol/SomeContract.jsonが生成されます。

3. デプロイの2つの方法

3.1 方法1: Foundry Scriptを使用

Deploy.s.solの作成

// foundry/script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;

import {Script, console} from "forge-std/Script.sol";
import {SomeContract} from "../src/SomeContract.sol";

contract DeployScript is Script {
    function run() external {
        vm.startBroadcast();
        
        SomeContract someContract = new SomeContract();
        
        console.log("Contract deployed at:", address(someContract));
        
        vm.stopBroadcast();
    }
}

重要な概念

  • external: 外部からのみ呼び出し可能(ガス効率が良い)
  • vm.startBroadcast(): トランザクションの収集を開始
  • new SomeContract(): コントラクトをインスタンス化(デプロイ)
  • vm.stopBroadcast(): トランザクションの収集を停止

デプロイコマンド

npm run deploy:foundry
# または
forge script foundry/script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  --broadcast

--broadcastフラグ:

  • なし: シミュレーションのみ(dry run)
  • あり: 実際にブロックチェーンに送信

3.2 方法2: TypeScriptを使用

deploy.tsの作成

// src/deploy.ts
import { 
    createPublicClient, 
    createWalletClient, 
    http 
  } from 'viem'
  import { foundry } from 'viem/chains'
  import { privateKeyToAccount } from 'viem/accounts'
  import contractJson from '../foundry/out/SomeContract.sol/SomeContract.json'
  
  const publicClient = createPublicClient({
    chain: foundry,
    transport: http('http://127.0.0.1:8545')
  })
  
  const account = privateKeyToAccount(
    '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
  )
  
  const walletClient = createWalletClient({
    account,
    chain: foundry,
    transport: http('http://127.0.0.1:8545')
  })
  
  async function deploy() {
    console.log('=== Deploy Starting ===')
    console.log('Deploying SomeContract...')
    
    // コントラクトをデプロイ
    const hash = await walletClient.deployContract({
      abi: contractJson.abi,
      bytecode: contractJson.bytecode.object as `0x${string}`,
      account
    })
  
    console.log('Transaction hash:', hash)
  
    // デプロイ完了を待つ
    const receipt = await publicClient.waitForTransactionReceipt({ hash })
    
    console.log('Contract deployed at:', receipt.contractAddress)
    console.log('Block number:', receipt.blockNumber.toString())
  
    return receipt.contractAddress
  }
  
  deploy().catch(error => {
    console.error('実行エラー:', error)
  })

重要なポイント

bytecodeの理解:

  • contractJson.bytecode.object: デプロイ用バイトコード(コンストラクタを含む)
  • as 0x${string}: TypeScriptの型アサーション(0xプレフィックス付き16進数文字列)
  • Foundryのコンパイル結果は自動的に0xプレフィックスが付く

なぜ2つの方法があるのか?:

  • Foundry Script: Solidityで全て処理、Foundryエコシステムとの統合が容易
  • TypeScript: より柔軟、アプリケーションからのデプロイに適している

4. 関数セレクターの理解

4.1 関数セレクターとは

Ethereumで関数を識別する4バイトのハッシュ値。

// src/function-selector.ts
import { keccak256, toBytes } from 'viem'

function getFunctionSelector(signature: string): string {
  const bytes = toBytes(signature)
  const hash = keccak256(bytes)
  return hash.slice(0, 10) // 最初の4バイト(0x + 8文字)
}

console.log('myUint() selector:', getFunctionSelector('myUint()'))
// 出力: 0x06540f7e

console.log('setUint(uint256) selector:', getFunctionSelector('setUint(uint256)'))
// 出力: 0x4ef65c3b

4.2 処理の流れ

  1. 関数シグネチャの文字列をバイト配列に変換: toBytes()
  2. Keccak256でハッシュ化: keccak256()
  3. 最初の4バイトを取得: slice(0, 10) (0x + 8文字 = 10文字)

4.3 使用例

// 低レベルAPIでコントラクトを呼び出す
const data = await publicClient.call({
  to: contractAddress,
  data: '0x06540f7e' // myUint()のfunction selector
})

5. ABIを使用した実践的な開発

5.1 with-abi-file.tsの作成

Foundryのコンパイル結果(JSON)からABIを読み込んで使用:

// src/with-abi-file.ts
import { 
    createPublicClient, 
    createWalletClient, 
    http,
    getContract
  } from 'viem'
  import { foundry } from 'viem/chains'
  import { privateKeyToAccount } from 'viem/accounts'
  import type { Address } from 'viem'
  import contractJson from '../foundry/out/SomeContract.sol/SomeContract.json'
  
  const contractAddress: Address = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
  
  const publicClient = createPublicClient({
    chain: foundry,
    transport: http('http://127.0.0.1:8545')
  })
  
  const account = privateKeyToAccount(
    '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
  )
  
  const walletClient = createWalletClient({
    account,
    chain: foundry,
    transport: http('http://127.0.0.1:8545')
  })
  
  async function main() {
    // FoundryのJSON出力からABIを取得
    const contract = getContract({
      address: contractAddress,
      abi: contractJson.abi,
      client: { public: publicClient, wallet: walletClient }
    })
  
    // 読み取り
    console.log('Reading myUint...')
    const value = await contract.read.myUint() as bigint
    console.log('Current value:', value.toString())
  
    // 書き込み
    console.log('\nUpdating to 100...')
    const hash = await contract.write.setUint([100n])
    await publicClient.waitForTransactionReceipt({ hash })
    
    // 確認
    const newValue = await contract.read.myUint() as bigint
    console.log('New value:', newValue.toString())
  }
  
  main().catch(console.error)

5.2 getContract()の重要性

const contract = getContract({
  address: contractAddress,
  abi: contractJson.abi,
  client: { public: publicClient, wallet: walletClient }
})

この関数が提供するもの:

  • contract.read.*: 型安全な読み取り関数
  • contract.write.*: 型安全な書き込み関数
  • 自動補完: IDEが関数名を提案
  • 型チェック: コンパイル時にエラーを検出

5.3 BigIntの重要性

const hash = await contract.write.setUint([100n])
//                                                 ^
//                                            このnが重要

なぜ100nなのか?:

  • 100: JavaScriptの通常の数値(number型)
  • 100n: BigInt型(任意精度の整数)
  • Solidityのuint256は最大2^256-1の値を扱える
  • JavaScriptの通常の数値では扱えないため、BigIntが必要

実際の違い:

typeof 100   // "number"
typeof 100n  // "bigint"

6. 開発フローの完成

6.1 完全な開発サイクル

# 1. Anvilの起動
anvil

# 2. コントラクトを編集
# foundry/src/SomeContract.sol を編集

# 3. コンパイル
cd foundry && forge build && cd ..

# 4. デプロイ
npx tsx src/deploy.ts

# 5. 対話(読み取り、書き込み)
npx tsx src/contract-interaction.ts

6.2 package.jsonのスクリプト

{
  "scripts": {
    "compile": "cd foundry && forge build && cd ..",
    "deploy:foundry": "cd foundry && forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast && cd ..",
    "deploy:ts": "tsx src/deploy.ts",
    "interact": "tsx src/contract-interaction.ts",
    "selector": "tsx src/function-selector.ts",
    "with-abi": "tsx src/with-abi-file.ts"
  }
}

7. 重要な概念の詳細

7.1 デプロイスクリプトの理解

vm.startBroadcast()vm.stopBroadcast():

  • startBroadcast(): トランザクションを収集開始
  • stopBroadcast(): トランザクション収集を停止
  • --broadcastフラグ: 収集したトランザクションを実際に送信

ブロードキャストの仕組み:

1. vm.startBroadcast() で収集モード開始
2. new SomeContract() でデプロイトランザクションを収集
3. vm.stopBroadcast() で収集終了
4. --broadcast フラグでチェーンに送信

7.2 bytecodeの種類

2種類のbytecode:

  • bytecode.object: デプロイ用(コンストラクタコードを含む、長い)
  • deployedBytecode.object: デプロイ後(コントラクトコードのみ、短い)

なぜ2種類あるのか?:

  • デプロイ時: コンストラクタが実行され、初期化コードが破棄される
  • デプロイ後: 実装コードのみがブロックチェーンに保存される

7.3 0xプレフィックス

なぜ必要?:

  • Ethereumの規格で、アドレス、bytecode、ハッシュは16進数で表記
  • 0xは16進数であることを示す
  • TypeScriptの型安全性(0x${string}

8. 実行例

8.1 function-selector.tsの実行

npx tsx src/function-selector.ts

出力:

myUint() selector: 0x06540f7e
setUint(uint256) selector: 0x4ef65c3b

8.2 with-abi-file.tsの実行

npx tsx src/with-abi-file.ts

出力:

Reading myUint...
Current value: 100

Updating to 100...
New value: 100

9. 学習のポイント

9.1 習得した技術

  1. FoundryとViemの統合
  2. 2種類のデプロイ方法
  3. 関数セレクターの計算と理解
  4. ABIの効果的な活用
  5. BigInt型の理解と使用

9.2 重要な理解事項

  1. Broadcast機能: シミュレーションと実際の送信の違い
  2. bytecodeの種類: デプロイ用とデプロイ後の違い
  3. 型システム: TypeScriptとBigIntの重要性
  4. ABIの利点: 型安全性と開発効率の向上

9.3 実践的なスキル

  1. スマートコントラクトのデプロイ
  2. コントラクトとの対話(読み取り・書き込み)
  3. 低レベルAPIと高レベルAPIの使い分け
  4. 関数セレクターの手動計算

10. まとめ

2025年10月26日は、FoundryとViemを組み合わせたスマートコントラクト開発の実践的な学習を行いました。デプロイの2つの方法、関数セレクター、ABIの活用など、本格的なブロックチェーン開発に必要な知識とスキルを習得しました。

10.1 重要な成果

  • 完全な開発サイクルの確立
  • デプロイ方法の理解と実践
  • 低レベルAPIと高レベルAPIの使い分け
  • 型安全性の確保

10.2 今後の学習方向

  • より複雑なスマートコントラクトの作成
  • イベントの監視と処理
  • マルチシグネチャとウォレット統合
  • ガス最適化の実践

Discussion