LoginSignup
2
2

More than 5 years have passed since last update.

クラウドファンディング向けEthereumコントラクトの仕様

Last updated at Posted at 2018-03-05

まず素朴なコントラクトを書いて、次にそのセキュリティ等の問題点を指摘して修正する。

想定するシチュエーション

  • こんな感じのクラウドファンディングサイトを構築する
  • 悪意あるプロジェクト立案(プロジェクト立案者の持ち逃げ)を阻止したい

基本ルール

  • プロジェクト立案者は、資金の使途・送金先に応じて送金requestを作成する
  • 送金requestをfinalizeして送金するには、出資者のうち過半数の合意が必要。

コントラクトの素朴な実装

Project.sol
contract Project{
    struct Request {
        string description;
        uint value;
        address recipient;
        bool complete;
        uint approvalCount;
        mapping(address => bool) approvals;
    }
    Request[] public requests;
    address public manager;
    uint public minimumContribution;
    mapping(address => bool) public approvers;
    uint public approversCount;

    modifier restricted() {
        require(msg.sender == manager);
        _;
    }
    function Project(uint minimum) public {
        manager = msg.sender;
        minimumContribution = minimum;
    }
    function contribute() public payable{
        require(msg.value > minimumContribution); 
        approvers[msg.sender] = true;
        approversCount++;
    }
    function createRequest(string description, uint value, address recipient) public restricted{
        Request memory newRequest = Request({
            description: description,
            value: value,
            recipient: recipient,
            complete: false,
            approvalCount: 0
        });
        requests.push(newRequest);
    }
    function approveRequest(uint index) public {
        Request storage request = requests[index];
        require(approvers[msg.sender]);
        require(!request.approvals[msg.sender]);
        request.approvals[msg.sender] = true;
        request.approvalCount++;
    }
    function finalizeRequest(uint index) public restricted{
        Request storage request = requests[index];
        require(request.approvalCount > (approversCount/2));
        require(!request.complete);
        request.recipient.transfer(request.value);
        request.complete = true;
    }
    function getSummay() public view returns(uint, uint, uint, uint, address){
        return(
            minimumContribution,
            this.balance,
            requests.length,
            approversCount,
            manager
        );
    }
    function getRequestsCount() public view returns(uint){
        return requests.length;
    }
}

素朴な実装と言いつつ工夫してあるのは、mappingを使っているところ。もし出資者全員分の配列を用意して、投票権・投票結果を記録する実装だと、出資者(出資口数)が増えるのに比例して計算量が増えてしまう。Ethereumでは仮想マシン(EVM)の命令ごとに定められた料金(gas)がかかるので、これは嬉しくない。上の実装ではmappingを使って、ハッシュ関数によって計算量を抑えるようにしてある。

仕様の問題点①

プロジェクトの乗っ取りが容易。
例えば、悪意のあるAさんがプロジェクト立案者となり1000万円出資を集めることができたとする。Aさんは出資者として自分のプロジェクトに自分のお金1001万円を追加出資。自分が過半数の票を持つので、任意の口座に全額2001万円を送金可能。

修正版の仕様では、一定の裁量を持つcritical_approverをプロジェクト立案時に設定する。critical_approverとしては例えばクラウドファンディングサイト運営者を選ぶ。送金requestをfinalizeするには「critical_approverが合意し、かつ、出資者の過半数が合意する」ことを条件とする。これにより明らかなSCAMを排除できる。

ただし、これではcritical_approverの権限が強すぎると考えられる場合は、送金requestをfinalizeするための条件を「(critical_approverが合意し、かつ、出資者の過半数が合意する)または(出資者の9割が合意する)」などとすることができる。これにより、たとえcritical_approverが拒否権を発動したとしても、圧倒的多数の支持があれば送金requestをfinalizeできる。その代わりに、先ほどのAさんの例でいうと、1000万円の出資に対して9001万円追加出資することで、プロジェクトの乗っ取りが可能になる。

ところで、実際はほとんどのプロジェクト立案者が善意であることを考えると、資金を使う際にいちいち過半数の出資者が合意しなくてはいけないのも面倒だ。そのため、後述する実装では、送金requestをfinalizeするための条件を「(critical_approverが合意し、かつ、出資者の4分の1が合意する)または(出資者の9割が合意する)」として、運営上の利便性向上を図った。

仕様の問題点②

クラウドファンディングサイト運営者側がデプロイする場合、費用等を負担することになるため、これはやりたくない。しかしながら、プロジェクト立案者にデプロイさせる場合、プロジェクトのすり替えが起きうる。この方法と対策を考える。

プロジェクト立案の手続きは素朴に考えると以下のようになる。はじめにプロジェクト立案者はこの(今作成しようとしている)クラウドファンディングサイトでコントラクトのソースコードを入手する。これをEthereumのネットワークにデプロイ。そのアドレスをクラウドファンディングサイトに登録。こうすることでサイトにプロジェクトを掲載できる。

この方法では、プロジェクトのすり替えが起きうる。例えば、悪意のあるAさんがこのクラウドファンディングサイトでプロジェクトを立案するとする。Aさんはクラウドファンディングサイトでコントラクトのソースコードを入手。これに対して悪意ある改造を加える。改造したソースコードを用いてEthereumのネットワークにデプロイ。そのアドレスをクラウドファンディングサイトに登録。こうすることでAさんは改造したプロジェクトをクラウドファンディングサイトに掲載できる。クラウドファンディングサイト運営者としては、コントラクトが正しいものであることをいちいち確かめるのは避けたい。

この解決方法として、Factoryコントラクトを実装する。まずクラウドファンディングサイト運営者はProjectFactoryをデプロイする。プロジェクト立案者はこのProjectFactoryのcreateProjectメソッドを実行することで新規プロジェクトを作成する。クラウドファンディングサイト運営者はProjectFactoryのgetProjectsメソッドを実行することで、正規の手続きでデプロイされたプロジェクトの一覧を得ることができる。

修正後のコントラクト

Project.sol
pragma solidity ^0.4.17;

contract ProjectFactory{
    address[] public deployedProjects;
    address public factoryManager;
    function ProjectFactory() public {
        factoryManager = msg.sender;
    }
    function createProject(uint minimum) public {
        address newProject = new Project(minimum, msg.sender, factoryManager);
        deployedProjects.push(newProject);
    }
    function getProjects() public view returns (address[]){
        return deployedProjects;
    }
}
contract Project{
    struct Request {
        string description;
        uint value;
        address recipient;
        bool complete;
        uint approvalCount;
        mapping(address => bool) approvals;
        bool critical_approve;
    }
    Request[] public requests;
    address public manager;
    uint public minimumContribution;
    mapping(address => bool) public approvers;
    uint public approversCount;
    address public critical_approver;

    modifier restricted() {
        require(msg.sender == manager);
        _;
    }
    function Project(uint minimum, address creator, address third_person) public {
        manager = creator;
        minimumContribution = minimum;
        critical_approver = third_person;
    }
    function contribute() public payable{
        require(msg.value > minimumContribution); 
        approvers[msg.sender] = true;
        approversCount++;
    }
    function createRequest(string description, uint value, address recipient) public restricted{
        Request memory newRequest = Request({
            description: description,
            value: value,
            recipient: recipient,
            complete: false,
            approvalCount: 0,
            critical_approve: false
        });
        requests.push(newRequest);
    }
    function approveRequest(uint index) public {
        Request storage request = requests[index];
        require(approvers[msg.sender]);
        require(!request.approvals[msg.sender]);
        request.approvals[msg.sender] = true;
        request.approvalCount++;
    }
    function approveRequest_critical(uint index) public {
        Request storage request = requests[index];
        require(critical_approver == msg.sender);
        request.critical_approve = true;
    }
    function finalizeRequest(uint index) public restricted{
        Request storage request = requests[index];
        bool cond0 = (request.approvalCount > (approversCount/4)) && request.critical_approve;
        bool cond1 = request.approvalCount > (approversCount*9/10);
        require(cond0 || cond1);
        require(!request.complete);
        request.recipient.transfer(request.value);
        request.complete = true;
    }
    function getSummay() public view returns(uint, uint, uint, uint, address, address){
        return(
            minimumContribution,
            this.balance,
            requests.length,
            approversCount,
            manager,
            critical_approver
        );
    }
    function getRequestsCount() public view returns(uint){
        return requests.length;
    }
}

参考資料

  • Stephen Grider, “Ethereum and Solidity: The Complete Developer's Guide”
  • Gas Costs from Yellow Paper -- EIP-150 Revision (1e18248 - 2017-04-12)
2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2