자주 쓰이는 패턴

컨트랙트에서의 출금

Effect 이후 기금송금에 있어 가장 권장되는 방법은 출금 패턴을 사용하는 것입니다. Effect의 결과로 Ether를 송금하는 가장 직관적인 방법은 직접 transfer 를 호출하는 것이겠지만, 잠재적인 보안위협을 초래 할 수 있으므로 권장하지 않습니다. security_consideration 페이지에서 보안에 대해 더 알아 볼 수 있습니다.

다음은 King of the Ether 에서 영감을 받아 작성된 "richest"가 되기 위해 가장 많은 돈을 컨트랙트로 송금하는 실제 출금패턴 예제입니다.

다음의 컨트랙트에서 당신이 "richest"를 빼앗긴다면, 새롭게 "richest" 가 된 사람으로부터 기금을 돌려 받을 것입니다.

pragma solidity >0.4.99 <0.6.0

contract WithdrawalContract {
    address public richest;
    uint public mostSent;

    mapping (address => uint) pendingWithdrawals;

    constructor() public payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable returns (bool) {
        if (msg.value > mostSent) {
            pendingWithdrawals[richest] += msg.value;
            richest = msg.sender;
            mostSent = msg.value;
            return true;
        } else {
            return false;
        }
    }

    function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // 리엔트란시(re-entrancy) 공격을 예방하기 위해
        // 송금하기 전에 보류중인 환불을 0으로 기억해 두십시오.
        pendingWithdrawals[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

다음은 직관적인 송금패턴과 정반대인 패턴입니다.

pragma solidity >0.4.99 <0.6.0;

contract SendContract {
    address payable public richest;
    uint public mostSent;

    constructor() public payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable returns (bool) {
        if (msg.value > mostSent) {
            // 현재의 라인이 문제의 원인이 될 수 있습니다. (아래에서 설명됨)
            richest.transfer(msg.value);
            richest = msg.sender;
            mostSent = msg.value;
            return true;
        } else {
            return false;
        }
    }
}

본 예제에서, 반드시 알아둬야 할 것은, 공격자가 실패 fallback함수를 가진 컨트랙트 주소를 richest 로 만들어 컨트랙트를 할 수 없는 상태로 만들 수 있다는 점입니다. (예를들어 revert() 를 사용하거나 단순히 2300 이상의 가스를 그들에게 전송 시켜 소비함으로써) 그렇게하면, "poisoned" 컨트랙트에 기금을 transfer 하기위해 송금이 요청 될 때마다 컨트랙트와 더불어 becomeRichest 도 실패 할 것이고, 이는 컨트랙트가 영원히 진행되지 않게 만들 것입니다.

반대로, 첫번째 예제에서 "withdraw" 패턴을 사용한다면 공격자의 출금만 실패할 것이고, 나머지 컨트랙트는 제대로 동작 할 것입니다.

제한된 액세스

제한된 액세스는 컨트랙트에서의 일반적인 패턴입니다. 알아둬야 할 것은, 다른사람이나 컴퓨터가 당신의 컨트랙트 상태의 내용을 읽는것을 결코 제한 할 수 없다는 것입니다. 암호화를 사용함으로써 컨트랙트를 더 읽기 어렵게 만들 수 있습니다. 하지만 컨트랙트가 데이터를 읽으려고 한다면, 다른 모든 사람들 또한 당신의 데이터를 읽을 수 있을것입니다.

다른 컨트랙트들 이 컨트랙트 상태를 읽지 못 하도록 권한을 제한 할 수 있습니다. 상태변수를 public 으로 선언하지 않는 한, 이 제한은 디폴트로 제공됩니다.

게다가, 컨트랙트 상태를 수정하거나 컨트랙트 함수를 호출 할 수 있는 사람을 제한 할 수 있습니다. 다음이 바로 본 섹션에 대한 내용입니다.

function modifiers 를 사용하면 이런 제한을 매우 알아보기 쉽게 할 수 있습니다.

pragma solidity >=0.4.22 <0.6.0;

contract AccessRestriction {
    // 이것들은 건설단계에서 할당됩니다.
    // 여기서, `msg.sender` 는
    // 이 계약을 생성하는 계정입니다.
    address public owner = msg.sender;
    uint public creationTime = now;

    // 수정자를 사용하여 함수의
    // 본문을 변경할 수 있습니다.
    // 이 수정자가 사용되면,
    // 함수가 특정 주소에서 호출 된
    // 경우에만 통과하는 검사가
    // 추가됩니다.
    modifier onlyBy(address _account)
    {
        require(
            msg.sender == _account,
            "Sender not authorized."
        );
        // "_;" 를 깜빡하지 마세요! 수정자가
        // 사용 될 때, "_;"가 실제 함수
        // 본문으로 대체됩니다.
        _;
    }

    /// `_newOwner` 를 이 컨트랙트의
    /// 새 소유자로 만듭니다.
    function changeOwner(address _newOwner)
        public
        onlyBy(owner)
    {
        owner = _newOwner;
    }

    modifier onlyAfter(uint _time) {
        require(
            now >= _time,
            "Function called too early."
        );
        _;
    }

    /// 소유권 정보를 지우십시오.
    /// 컨트랙트가 생성된 후 6주가
    /// 지나야 호출 될 수 있습니다.
    function disown()
        public
        onlyBy(owner)
        onlyAfter(creationTime + 6 weeks)
    {
        delete owner;
    }

    // 이 수정자는 함수 호출과 관련된
    // 특정 요금을 요구합니다.
    // 호출자가 너무 많은 금액을 송금했을시,
    // 함수 본문 이후에만 환급이 됩니다.
    // 이는 Solidity 0.4.0 이전의 버전에서는 위험하였습니다.
    // `_;` 이후의 부분은 스킵될 가능성이 있습니다.
    modifier costs(uint _amount) {
        require(
            msg.value >= _amount,
            "Not enough Ether provided."
        );
        _;
        if (msg.value > _amount)
            msg.sender.transfer(msg.value - _amount);
    }

    function forceOwnerChange(address _newOwner)
        public
        payable
        costs(200 ether)
    {
        owner = _newOwner;
        // 몇 가지의 예제 조건
        if (uint(owner) & 0 == 1)
            // 이는 Solidity 0.4.0 이전의
            // 버전에서는 환불되지 않았습니다.
            return;
        // 초과 요금에 대한 환불
    }
}

함수호출에 대한 액세스를 제한 할 수 있는 보다 특별한 방법에 대해서는 다음 예제에서 설명합니다.

상태 머신

컨트랙트는 종종 상태머신인 것처럼 동작합니다. 다시 말해, 컨트랙트들이 다르게 동작하거나 다른 함수들에 의해 호출되는 어떠한 단계 를 가지고 있습니다. 함수 호출은 종종 단계를 끝내고 컨트랙트를 다음 단계로 전환 시킵니다. (특히 컨트랙트 모델이 상호작용 인 경우에) 또한, 시간 의 특정 지점에서 일부 단계에 자동으로 도달 하는 것이 일반적입니다.

예를 들어 "블라인드 입찰을 수락하는" 단계에서 시작하여 "옥션 결과 결정"으로 끝나는 "공개 입찰"로 전환하는 블라인드 옥션 컨트랙트가 있습니다.

이 상황에서 상태를 모델링하고 컨트랙트의 잘못된 사용을 방지하기 위해 함수 수정자를 사용할 수 있습니다.

예제

다음의 예제에서, 수정자 atStage 는 함수가 특정 단계에서만 호출되도록 보장해 줍니다.

자동 timed transitions 는 모든 함수에서 사용되는 수정자 timeTransitions 에 의해 처리됩니다.

마지막으로, 수정자 transitionNext 는 함수가 끝났을 때, 자동적으로 다음 단계로 넘어가도록 하기 위해 사용될 수 있습니다.

pragma solidity >=0.4.22 <0.6.0;

contract StateMachine {
    enum Stages {
        AcceptingBlindedBids,
        RevealBids,
        AnotherStage,
        AreWeDoneYet,
        Finished
    }

    // 이 부분이 현재의 단계입니다.
    Stages public stage = Stages.AcceptingBlindedBids;

    uint public creationTime = now;

    modifier atStage(Stages _stage) {
        require(
            stage == _stage,
            "Function cannot be called at this time."
        );
        _;
    }

    function nextStage() internal {
        stage = Stages(uint(stage) + 1);
    }

    // timed transitions를 수행하십시오.
    // 이 수정자를 먼저 언급해야 합니다. 그렇지 않으면,
    // Guards가 새로운 단계를 고려하지 않을 것 입니다.
    modifier timedTransitions() {
        if (stage == Stages.AcceptingBlindedBids &&
                    now >= creationTime + 10 days)
            nextStage();
        if (stage == Stages.RevealBids &&
                now >= creationTime + 12 days)
            nextStage();
        // 다른 단계는 거래에 의해 전환됩니다.
        _;
    }

    // 수정자의 순서가 중요합니다!
    function bid()
        public
        payable
        timedTransitions
        atStage(Stages.AcceptingBlindedBids)
    {
        // 우리는 여기서 그것을 구현하지 않을 것입니다.
    }

    function reveal()
        public
        timedTransitions
        atStage(Stages.RevealBids)
    {
    }

    // 이 수정자는 함수가 완료된 후
    // 다음 단계로 이동합니다.
    modifier transitionNext()
    {
        _;
        nextStage();
    }

    function g()
        public
        timedTransitions
        atStage(Stages.AnotherStage)
        transitionNext
    {
    }

    function h()
        public
        timedTransitions
        atStage(Stages.AreWeDoneYet)
        transitionNext
    {
    }

    function i()
        public
        timedTransitions
        atStage(Stages.Finished)
    {
    }
}