スマコンをデプロイするスマコンのお試し実装

# はじめに
最近、非中央集権的なコミュニティにおいて合意を取ってスマートコントラクトをデプロイする方法について考える機会がありました。
この方法について簡単ですが実装してみたので、メモとしてアウトプットしたいと思います。

# やったこと
スマコンをデプロイするためのスマコン(Deployer)を作り、そのスマコンを通して合意が取れた新スマコンをデプロイする、ということをやってみました。
以下がイメージ図です。この内容を簡単です実装しました。
※スマコンはほぼMicrosoft Copilotに書いてもらいました。
![deployer_multisig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624762/b943bf02-9ad5-44e1-a509-7eb66caef213.png)

# 実施手順
## 前提条件
- Hardhatのインストール

## 前準備
Hardhatプロジェクトとネットワークの準備をしておきます。このHardhatネットワークに対してスマコンをデプロイします。
```
cd <お好きなディレクトリ>
npx hardhat init　※ここでは「Create an empty hardhat.config.js」を選択します
mkdir contracts scripts
npx hardhat node
```

## 実行手順

### 1. Deployer&Multisigの準備
以下の2つのスマコンを`contracts`ディレクトリに格納します。
- Deployer.sol (スマコンをデプロイするスマコン)
    ```
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    import "./MultiSig.sol";
    
    contract Deployer is MultiSig {
        event ContractDeployed(address indexed contractAddress);
    
        mapping(uint256 => bytes) public storedBytecode;
        mapping(uint256 => address) public deployedContracts;
        uint256 public bytecodeCounter;
    
        constructor(address[] memory _owners, uint256 _requiredSignatures)
            MultiSig(_owners, _requiredSignatures)
        {}
    
        function storeBytecode(bytes memory bytecode) public onlyOwner returns (uint256) {
            bytecodeCounter++;
            storedBytecode[bytecodeCounter] = bytecode;
            return bytecodeCounter;
        }
    
        function deployStoredBytecode(uint256 bytecodeId) public returns (address) {
            require(_isApproved(bytecodeId), "Not enough approvals");
    
            bytes memory bytecode = storedBytecode[bytecodeId];
            require(bytecode.length > 0, "Bytecode not found");
    
            address contractAddress;
            assembly {
                contractAddress := create(0, add(bytecode, 0x20), mload(bytecode))
            }
            require(contractAddress != address(0), "Deployment failed");
    
            deployedContracts[bytecodeId] = contractAddress;
            emit ContractDeployed(contractAddress);
            return contractAddress;
        }
    
        function getStoredBytecode(uint256 bytecodeId) public view returns (bytes memory) {
            return storedBytecode[bytecodeId];
        }
    
        function getDeployedContract(uint256 bytecodeId) public view returns (address) {
            return deployedContracts[bytecodeId];
        }
    }
    ```
- MultiSig.sol (複数署名用のスマコン)
    ```
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    abstract contract MultiSig {
        event Test(string testStr);
    
        address[] public owners;
        uint256 public requiredSignatures;
        mapping(uint256 => mapping(address => bool)) public approvals;
    
        constructor(address[] memory _owners, uint256 _requiredSignatures) {
            require(_owners.length > 0, "Owners required");
            require(_requiredSignatures > 0 && _requiredSignatures <= _owners.length, "Invalid number of required signatures");
    
            owners = _owners;
            requiredSignatures = _requiredSignatures;
        }
    
        modifier onlyOwner() {
            bool isOwner = false;
            for (uint256 i = 0; i < owners.length; i++) {
                if (owners[i] == msg.sender) {
                    isOwner = true;
                    break;
                }
            }
            require(isOwner, "Not an owner");
            _;
        }
    
        function approveDeployment(uint256 bytecodeId) public onlyOwner {
            approvals[bytecodeId][msg.sender] = true;
            emit Test("approve success");
        }
    
        function isApproved(uint256 bytecodeId) public view returns (bool) {
            return _isApproved(bytecodeId);
        }
    
        function _isApproved(uint256 bytecodeId) internal view returns (bool) {
            uint256 count = 0;
            for (uint256 i = 0; i < owners.length; i++) {
                if (approvals[bytecodeId][owners[i]]) {
                    count++;
                }
                if (count >= requiredSignatures) {
                    return true;
                }
            }
            return false;
        }
    }
    ```

### 2. テストスマコンの用意 (SimpleStorage)
以下のテスト用のスマコンを`contracts`ディレクトリに格納します。
- SimpleStorage.sol (Deployerを通してデプロイするためのテストスマコン)
    ```
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract SimpleStorage {
        uint256 public storedData;
    
        event DataStored(uint256 data);
    
        function set(uint256 x) public {
            storedData = x;
            emit DataStored(x);
        }
    
        function get() public view returns (uint256) {
            return storedData;
        }
    }
    ```
    
### 3. 動作確認
1. スクリプトの準備
`Deployer`コントラクトをデプロイし、このコントラクトを通して`SimpleStorage`コントラクトをデプロイするためのスクリプトです。名前は`run.ts`で、`scripts`配下に格納します。
    ```
    // Import necessary libraries and contracts
    import { ethers } from "hardhat";
    
    async function main() {
        // Hardhatが用意してくれているアカウントを3つ取ってきます
        const [owner1, owner2, owner3] = await ethers.getSigners();
    
        // Deployerコントラクトをデプロイします
        // owner1~3の内、2人以上の署名が必要な設定にしています
        const DeployerFactory = await ethers.getContractFactory("Deployer");
        const deployerContract = await DeployerFactory.deploy([owner1.address, owner2.address, owner3.address], 2);
        await deployerContract.waitForDeployment();
        console.log("Deployer deployed to:", deployerContract.target);
    
        // SimpleStorageコントラクトのバイトコードを準備します
        const SimpleStorageFactory = await ethers.getContractFactory("SimpleStorage");
        const simpleStorageBytecode = SimpleStorageFactory.bytecode;
    
        // DeployerコントラクトにSimpleStorageコントラクトのバイトコードを一旦格納します
        await deployerContract.connect(owner1).storeBytecode(simpleStorageBytecode);
        const storedBytecode = await deployerContract.getStoredBytecode(1);
        console.log("Deployed bytecode for bytecode ID:", 1, "is:", storedBytecode);
    
        // owner2と3のデプロイ許可(署名)を取ります
        await deployerContract.connect(owner2).approveDeployment(1);
        await deployerContract.connect(owner3).approveDeployment(1);
    
        // Deployerコントラクトを通して一時格納されたSimpleStorageコントラクトをデプロイします
        const tx = await deployerContract.deployStoredBytecode(1);
        const receipt = await tx.wait();
        const simpleStorageAddress = receipt.logs.find(eventLogs => eventLogs.fragment.name === "ContractDeployed").args[0];
        console.log("SimpleStorage deployed to:", simpleStorageAddress);
    
        // デプロイされたSimpleStorageコントラクトのアドレスを取得します
        const storedAddress = await deployerContract.getDeployedContract(1);
        console.log("Stored address for bytecode ID", 1, "is:", storedAddress);
    
        // デプロイされたSimpleStorageコントラクトに値を格納できるか確認します
        const simpleStorageContract = await ethers.getContractAt("SimpleStorage", simpleStorageAddress);
        await simpleStorageContract.set(17);
        const storedData = await simpleStorageContract.get();
        console.log("Stored data in SimpleStorage contract:", storedData.toString());
    }
    
    main()
        .then(() => process.exit(0))
        .catch(error => {
            console.error(error);
            process.exit(1);
        });
    ```
1. スクリプトの実行
以下のコマンドでスクリプトを実行し、エラー無く動くか確認します。
    ```
    npx hardhat run scripts/run.ts --network localhost
    ```
以下のような出力になるはずです。
```
Deployer deployed to: 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
Deployed bytecode for bytecode ID: 1 is: 0x6080604052348015600f57600080fd5b506101b68061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80632a1afcd91461004657806360fe47b1146100645780636d4ce63c14610080575b600080fd5b61004e61009e565b60405161005b9190610107565b60405180910390f35b61007e60048036038101906100799190610153565b6100a4565b005b6100886100e5565b6040516100959190610107565b60405180910390f35b60005481565b806000819055507f9455957c3b77d1d4ed071e2b469dd77e37fc5dfd3b4d44dc8a997cc97c7b3d49816040516100da9190610107565b60405180910390a150565b60008054905090565b6000819050919050565b610101816100ee565b82525050565b600060208201905061011c60008301846100f8565b92915050565b600080fd5b610130816100ee565b811461013b57600080fd5b50565b60008135905061014d81610127565b92915050565b60006020828403121561016957610168610122565b5b60006101778482850161013e565b9150509291505056fea2646970667358221220e66f51ea7677bca32c12363b9b8620b1e3d6981350b5842d04826695c2042da564736f6c634300081c0033
SimpleStorage deployed to: 0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321
Stored address for bytecode ID 1 is: 0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321
Stored data in SimpleStorage contract: 17
```

# まとめ
- スマコンをデプロイするスマコンが作れる
- スマコンのデプロイを実際に実施するには複数署名が必要にできる

以上のことから合意を取ってからスマコンをデプロイする、ということはできそうだなと思いました。
実運用するにはデプロイ同意の判断となるスマコンの検証方法やデプロイ後のコントラクトの無効化など、まだ考えることはありそうですが少しでも実際に動かすことができてよかったです。
また、別の考え方をすればERC20などのガバナンストークンを使った投票システムでも実現できるのでそちらのパターンも今後やってみたいと思います。