Solidity v0.5.0 Breaking Changes

Solidity v0.5.0 Breaking Changes

이 절은 Solidity 버전 0.5.0에서 도입된 주요 breaking change와, 그 변경의 이유, 그리고 영향을 받는 코드를 업데이트하는 방법을 강조해요. 전체 목록은 릴리스 changelog를 확인해요. v0.5.0부터 명시적 가시성·데이터 위치가 필수가 되고 address payable 타입이 분리되는 등 코드를 더 명확하게 만드는 변경이 많았어요.

출처: 문서

본문

이 절은 Solidity 버전 0.5.0에서 도입된 주요 breaking change와 그 뒤의 이유, 그리고 영향을 받는 코드를 업데이트하는 방법을 강조해요. 전체 목록은 릴리스 changelog를 확인해요.

참고 (Note)

Solidity v0.5.0으로 컴파일된 컨트랙트는 여전히 재컴파일하거나 재배포하지 않고도 이전 버전으로 컴파일된 컨트랙트, 심지어 라이브러리와도 인터페이스할 수 있어요. 인터페이스에 데이터 위치와 가시성·가변성 지정자를 포함하도록 바꾸는 것으로 충분해요. 아래의 이전 컨트랙트와의 상호운용성(Interoperability With Older Contracts) 절을 참고해요.

의미만 바뀐 변경 (Semantic Only Changes)

이 절은 의미적으로만 바뀌어 기존 코드에 새롭고 다른 동작을 숨길 수 있는 변경을 나열해요.

  • 부호 있는 오른쪽 시프트는 이제 0으로 반올림하는 대신 음의 무한대로 반올림하는 제대로 된 산술 시프트를 사용해요. 부호 있는 시프트와 부호 없는 시프트는 Constantinople에서 전용 opcode를 가지며, 현재는 Solidity가 에뮬레이션해요.
  • do...while 루프의 continue 문은 이제 조건으로 점프해요. 이것이 그런 경우의 흔한 동작이에요. 예전에는 루프 본문으로 점프했어요. 조건이 거짓이면 루프가 종료돼요.
  • .call(), .delegatecall(), .staticcall() 함수는 이제 단일 bytes 매개변수가 주어질 때 더 이상 패딩하지 않아요.
  • pure와 view 함수는 EVM 버전이 Byzantium 이상이면 CALL 대신 opcode STATICCALL로 호출돼요. 이는 EVM 수준에서 상태 변경을 허용하지 않아요.
  • ABI 인코더는 이제 외부 함수 호출과 abi.encode에서 사용될 때 콜데이터(msg.data와 외부 함수 매개변수)의 바이트 배열과 문자열을 제대로 패딩해요. 패딩되지 않은 인코딩에는 abi.encodePacked를 사용해요.
  • ABI 디코더는 전달된 콜데이터가 너무 짧거나 범위를 벗어나면 함수의 시작과 abi.decode()에서 revert해요. 더티한 상위 비트는 여전히 그냥 무시된다는 점에 주의해요.
  • Tangerine Whistle부터 외부 함수 호출에 사용 가능한 모든 가스를 전달해요.

의미·문법 변경 (Semantic and Syntactic Changes)

이 절은 문법과 의미에 영향을 주는 변경을 강조해요.

  • .call(), .delegatecall(), staticcall(), keccak256(), sha256(), ripemd160() 함수는 이제 단일 bytes 인자만 받아요. 게다가 인자는 패딩되지 않아요. 이는 인자가 어떻게 연결되는지 더 명확하게 보이도록 바뀐 것이에요. 모든 .call()(과 그 계열)을 .call("")로 바꾸고, 모든 .call(signature, a, b, c)를 .call(abi.encodeWithSignature(signature, a, b, c))로 바꿔요(마지막 것은 값 타입에만 동작). 모든 keccak256(a, b, c)을 keccak256(abi.encodePacked(a, b, c))로 바꿔요. breaking change는 아니지만, 개발자들은 x.call(bytes4(keccak256("f(uint256)")), a, b)를 x.call(abi.encodeWithSignature("f(uint256)", a, b))로 바꾸는 걸 권장해요.
  • .call(), .delegatecall(), .staticcall() 함수는 이제 반환 데이터에 접근할 수 있도록 (bool, bytes memory)를 반환해요. bool success = otherContract.call("f")를 (bool success, bytes memory data) = otherContract.call("f")로 바꿔요.
  • Solidity는 이제 함수 로컬 변수에 C99 스타일의 스코프 규칙을 구현해요. 즉 변수는 선언된 후에만, 그리고 같은 또는 중첩된 스코프에서만 사용될 수 있어요. for 루프의 초기화 블록에서 선언된 변수는 루프 안 어디서든 유효해요.

명시성 요구사항 (Explicitness Requirements)

이 절은 코드가 이제 더 명시적이어야 하는 변경을 나열해요. 대부분의 주제에 대해 컴파일러가 제안을 제공할 거예요.

  • 명시적 함수 가시성이 이제 필수예요. 모든 함수와 생성자에 public을, 가시성을 지정하지 않은 모든 fallback 또는 인터페이스 함수에 external을 추가해요.
  • 구조체, 배열, 매핑 타입의 모든 변수에 대한 명시적 데이터 위치가 이제 필수예요. 이는 함수 매개변수와 반환 변수에도 적용돼요. 예를 들어 uint[] x = z를 uint[] storage x = z로 바꾸고, function f(uint[][] x)를 function f(uint[][] memory x)로 바꿔요. 여기서 memory는 데이터 위치이고 상황에 따라 storage나 calldata로 교체될 수 있어요. 외부 함수는 calldata 데이터 위치의 매개변수를 요구한다는 점에 주의해요.
  • 네임스페이스를 분리하기 위해 컨트랙트 타입은 더 이상 address 멤버를 포함하지 않아요. 따라서 이제 address 멤버를 사용하기 전에 컨트랙트 타입의 값을 address로 명시적으로 변환해야 해요. 예: c가 컨트랙트라면 c.transfer(...)를 address(c).transfer(...)로, c.balance를 address(c).balance로 바꿔요.
  • 관련 없는 컨트랙트 타입 사이의 명시적 변환은 이제 허용되지 않아요. 컨트랙트 타입에서 그 기본 또는 조상 타입 중 하나로만 변환할 수 있어요. 컨트랙트가 상속하지 않지만 변환하려는 컨트랙트 타입과 호환된다고 확신하면, 먼저 address로 변환해 우회할 수 있어요. 예: A와 B가 컨트랙트 타입이고, B가 A를 상속하지 않으며, b가 타입 B의 컨트랙트라면, A(address(b))로 b를 타입 A로 변환할 수 있어요. 아래에서 설명하듯 일치하는 payable fallback 함수를 주의해야 한다는 점을 잊지 마세요.
  • address 타입은 address와 address payable로 분리됐고, transfer 함수는 address payable만 제공해요. address payable은 address로 직접 변환될 수 있지만 반대는 허용되지 않아요. address를 address payable로 변환하는 것은 uint160을 통한 변환으로 가능해요. c가 컨트랙트라면, address(c)는 c가 payable fallback 함수를 가질 때만 address payable이 돼요. 출금 패턴을 사용한다면 transfer가 저장된 주소가 아니라 msg.sender에만 사용되고 msg.sender는 address payable이므로 코드를 바꿀 필요가 없을 가능성이 높아요.
  • 크기가 다른 bytesX와 uintY 사이의 변환은 이제 허용되지 않아요. bytesX는 오른쪽에, uintY는 왼쪽에 패딩돼 예상치 못한 변환 결과를 만들 수 있기 때문이에요. 이제 변환 전에 타입 안에서 크기를 조정해야 해요. 예를 들어 bytes4(4바이트)를 uint64(8바이트)로 변환하려면 먼저 bytes4 변수를 bytes8로 변환한 다음 uint64로 변환할 수 있어요. uint32를 통해 변환하면 반대 패딩을 얻어요. v0.5.0 이전에는 bytesX와 uintY 사이의 어떤 변환이든 uint8X를 거쳤어요. 예를 들어 uint8(bytes3(0x291807))은 uint8(uint24(bytes3(0x291807)))으로 변환됐어요(결과는 0x07).
  • 보안 기능으로, non-payable 함수에서 msg.value를 사용하는 것(또는 수정자를 통해 도입하는 것)이 허용되지 않아요. 함수를 payable로 만들거나, msg.value를 사용하는 프로그램 로직을 위한 새 내부 함수를 만들어요.
  • 명확성 이유로, 명령줄 인터페이스는 이제 소스로 표준 입력을 사용할 때 -을 요구해요.

비권장 요소 (Deprecated Elements)

이 절은 이전 기능이나 문법을 비권장하는 변경을 나열해요. 이 변경 중 다수는 이미 실험적 모드 v0.5.0에서 활성화됐다는 점에 주의해요.

명령줄과 JSON 인터페이스 (Command-line and JSON Interfaces)

  • 명령줄 옵션 --formal(추가 형식 검증을 위한 Why3 출력 생성에 사용)은 비권장됐고 이제 제거됐어요. 새 형식 검증 모듈인 SMTChecker는 pragma experimental SMTChecker;으로 활성화돼요.
  • 중간 언어 Julia에서 Yul로 개명됨에 따라 명령줄 옵션 --julia가 --yul로 개명됐어요.
  • --clone-bin과 --combined-json clone-bin 명령줄 옵션이 제거됐어요.
  • 빈 prefix를 가진 리매핑을 허용하지 않아요.
  • JSON AST 필드 constant와 payable이 제거됐어요. 이 정보는 이제 stateMutability 필드에 있어요.
  • FunctionDefinition 노드의 JSON AST 필드 isConstructor는 "constructor", "fallback" 또는 "function" 값을 가질 수 있는 kind 필드로 교체됐어요.
  • 링크되지 않은 바이너리 hex 파일에서, 라이브러리 주소 플레이스홀더는 이제 완전 정규화된 라이브러리 이름의 keccak256 해시의 첫 36개 hex 문자이며, $...$로 둘러싸여 있어요. 이전에는 단지 완전 정규화된 라이브러리 이름만 사용됐어요. 이는 특히 긴 경로를 사용할 때 충돌 가능성을 줄여줘요. 바이너리 파일은 이제 이런 플레이스홀더에서 완전 정규화된 이름으로의 매핑 목록도 포함해요.

생성자 (Constructors)

  • 생성자는 이제 constructor 키워드로 정의해야 해요.
  • 괄호 없이 기본 생성자를 호출하는 것을 이제 허용하지 않아요.
  • 같은 상속 계층에서 기본 생성자 인자를 여러 번 지정하는 것을 이제 허용하지 않아요.
  • 인자가 있는데 잘못된 인자 개수로 생성자를 호출하는 것을 이제 허용하지 않아요. 인자를 주지 않고 상속 관계만 지정하려면 괄호를 아예 제공하지 마세요.

함수 (Functions)

  • callcode 함수 호출은 이제 허용되지 않아요(delegatecall을 권장). 인라인 어셈블리를 통해 여전히 사용할 수 있어요.
  • suicide는 이제 허용되지 않아요(selfdestruct를 권장).
  • sha3는 이제 허용되지 않아요(keccak256을 권장).
  • throw는 이제 허용되지 않아요(revert, require, assert를 권장).

변환 (Conversions)

  • 십진 리터럴에서 bytesXX 타입으로의 명시적·암시적 변환이 이제 허용되지 않아요.
  • 크기가 다른 hex 리터럴에서 bytesXX 타입으로의 명시적·암시적 변환이 이제 허용되지 않아요.

리터럴과 접미사 (Literals and Suffixes)

  • 윤년에 대한 복잡성과 혼란 때문에 단위 명칭 years가 이제 허용되지 않아요.
  • 숫자가 뒤따르지 않는 끝점(dot)이 이제 허용되지 않아요.
  • hex 숫자와 단위 명칭을 결합하는 것(예: 0x1e wei)이 이제 허용되지 않아요.
  • hex 숫자의 접두사 0X가 허용되지 않아요. 0x만 가능해요.

변수 (Variables)

  • 명확성을 위해 빈 구조체 선언이 이제 허용되지 않아요.
  • 명확성을 위해 var 키워드가 이제 허용되지 않아요.
  • 구성 요소 수가 다른 튜플 사이의 할당이 이제 허용되지 않아요.
  • 컴파일 타임 상수가 아닌 상수의 값이 허용되지 않아요.
  • 값 수가 일치하지 않는 다중 변수 선언이 이제 허용되지 않아요.
  • 초기화되지 않은 스토리지 변수가 이제 허용되지 않아요.
  • 빈 튜플 구성 요소가 이제 허용되지 않아요.
  • 변수와 구조체의 순환 의존성 감지는 재귀 256으로 제한돼요.
  • 길이가 0인 고정 크기 배열이 이제 허용되지 않아요.

문법 (Syntax)

  • 함수 상태 가변성 수정자로 constant를 사용하는 것이 이제 허용되지 않아요.
  • 불리언 표현식은 산술 연산을 사용할 수 없어요.
  • 단항 + 연산자가 이제 허용되지 않아요.
  • 리터럴은 명시적 타입으로의 변환 없이는 더 이상 abi.encodePacked와 함께 사용될 수 없어요.
  • 하나 이상의 반환 값을 가진 함수의 빈 return 문이 이제 허용되지 않아요.
  • "loose assembly" 문법이 이제 완전히 허용되지 않아요. 즉 점프 레이블, 점프, 비-함수형 명령을 더 이상 사용할 수 없어요. 대신 새 while, switch, if 구문을 사용해요.
  • 구현이 없는 함수는 더 이상 수정자를 사용할 수 없어요.
  • 이름 있는 반환 값을 가진 함수 타입이 이제 허용되지 않아요.
  • if/while/for 본문 안에서 블록이 아닌 단일 문 변수 선언이 이제 허용되지 않아요.
  • 새 키워드: calldata와 constructor.
  • 새 예약 키워드: alias, apply, auto, copyof, define, immutable, implements, macro, mutable, override, partial, promise, reference, sealed, sizeof, supports, typedef, unchecked.

이전 컨트랙트와의 상호운용성 (Interoperability With Older Contracts)

v0.5.0 이전 Solidity 버전으로 작성된 컨트랙트와(또는 그 반대로) 인터페이스를 정의해 통신하는 것은 여전히 가능해요. 이미 배포된 다음 pre-0.5.0 컨트랙트를 생각해 볼게요:

open in Remix

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// This will report a warning until version 0.4.25 of the compiler
// This will not compile after 0.5.0
contract OldContract {
    function someOldFunction(uint8 a) {
        //...
    }
    function anotherOldFunction() constant returns (bool) {
        //...
    }
    // ...
}

이것은 Solidity v0.5.0으로 더 이상 컴파일되지 않아요. 그러나 호환되는 인터페이스를 정의할 수 있어요:

open in Remix

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
    function someOldFunction(uint8 a) external;
    function anotherOldFunction() external returns (bool);
}

원래 컨트랙트에서 anotherOldFunction이 constant로 선언됐는데도 우리가 view로 선언하지 않았다는 점에 주의해요. 이는 Solidity v0.5.0부터 view 함수를 호출하는 데 staticcall이 사용되기 때문이에요. v0.5.0 이전에는 constant 키워드가 강제되지 않았으므로, constant로 선언된 함수를 staticcall로 호출하면 그 constant 함수가 여전히 스토리지 수정을 시도할 수 있어 여전히 revert할 수 있어요. 따라서 이전 컨트랙트의 인터페이스를 정의할 때는 함수가 staticcall로 동작할 것이 절대적으로 확실한 경우에만 constant 대신 view를 사용해야 해요.

위에서 정의한 인터페이스가 주어지면, 이미 배포된 pre-0.5.0 컨트랙트를 쉽게 사용할 수 있어요:

open in Remix

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

interface OldContract {
    function someOldFunction(uint8 a) external;
    function anotherOldFunction() external returns (bool);
}

contract NewContract {
    function doSomething(OldContract a) public returns (bool) {
        a.someOldFunction(0x42);
        return a.anotherOldFunction();
    }
}

마찬가지로, pre-0.5.0 라이브러리는 라이브러리의 함수를 구현 없이 정의하고 링크하는 동안 pre-0.5.0 라이브러리의 주소를 제공하면 사용할 수 있어요(링크에 명령줄 컴파일러를 사용하는 방법은 명령줄 컴파일러 사용하기 참고):

open in Remix

// This will not compile after 0.6.0
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;

library OldLibrary {
    function someFunction(uint8 a) public returns(bool);
}

contract NewContract {
    function f(uint8 a) public returns (bool) {
        return OldLibrary.someFunction(a);
    }
}

예시 (Example)

다음 예시는 이 절에 나열된 일부 변경이 적용된, 컨트랙트와 Solidity v0.5.0용 업데이트 버전을 보여줘요.

옛 버전:

open in Remix

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// This will not compile after 0.5.0

contract OtherContract {
    uint x;
    function f(uint y) external {
        x = y;
    }
    function() payable external {}
}

contract Old {
    OtherContract other;
    uint myNumber;

    // Function mutability not provided, not an error.
    function someInteger() internal returns (uint) { return 2; }

    // Function visibility not provided, not an error.
    // Function mutability not provided, not an error.
    function f(uint x) returns (bytes) {
        // Var is fine in this version.
        var z = someInteger();
        x += z;
        // Throw is fine in this version.
        if (x > 100)
            throw;
        bytes memory b = new bytes(x);
        y = -3 >> 1;
        // y == -1 (wrong, should be -2)
        do {
            x += 1;
            if (x > 10) continue;
            // 'Continue' causes an infinite loop.
        } while (x < 11);
        // Call returns only a Bool.
        bool success = address(other).call("f");
        if (!success)
            revert();
        else {
            // Local variables could be declared after their use.
            int y;
        }
        return b;
    }

    // No need for an explicit data location for 'arr'
    function g(uint[] arr, bytes8 x, OtherContract otherContract) public {
        otherContract.transfer(1 ether);

        // Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
        // the first 4 bytes of x will be lost. This might lead to
        // unexpected behavior since bytesX are right padded.
        uint32 y = uint32(x);
        myNumber += y + msg.value;
    }
}

새 버전:

open in Remix

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
// This will not compile after 0.6.0

contract OtherContract {
    uint x;
    function f(uint y) external {
        x = y;
    }
    function() payable external {}
}

contract New {
    OtherContract other;
    uint myNumber;

    // Function mutability must be specified.
    function someInteger() internal pure returns (uint) { return 2; }

    // Function visibility must be specified.
    // Function mutability must be specified.
    function f(uint x) public returns (bytes memory) {
        // The type must now be explicitly given.
        uint z = someInteger();
        x += z;
        // Throw is now disallowed.
        require(x <= 100);
        int y = -3 >> 1;
        require(y == -2);
        do {
            x += 1;
            if (x > 10) continue;
            // 'Continue' jumps to the condition below.
        } while (x < 11);

        // Call returns (bool, bytes).
        // Data location must be specified.
        (bool success, bytes memory data) = address(other).call("f");
        if (!success)
            revert();
        return data;
    }

    using AddressMakePayable for address;
    // Data location for 'arr' must be specified
    function g(uint[] memory /* arr */, bytes8 x, OtherContract otherContract, address unknownContract) public payable {
        // 'otherContract.transfer' is not provided.
        // Since the code of 'OtherContract' is known and has the fallback
        // function, address(otherContract) has type 'address payable'.
        address(otherContract).transfer(1 ether);

        // 'unknownContract.transfer' is not provided.
        // 'address(unknownContract).transfer' is not provided
        // since 'address(unknownContract)' is not 'address payable'.
        // If the function takes an 'address' which you want to send
        // funds to, you can convert it to 'address payable' via 'uint160'.
        // Note: This is not recommended and the explicit type
        // 'address payable' should be used whenever possible.
        // To increase clarity, we suggest the use of a library for
        // the conversion (provided after the contract in this example).
        address payable addr = unknownContract.makePayable();
        require(addr.send(1 ether));

        // Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
        // the conversion is not allowed.
        // We need to convert to a common size first:
        bytes4 x4 = bytes4(x); // Padding happens on the right
        uint32 y = uint32(x4); // Conversion is consistent
        // 'msg.value' cannot be used in a 'non-payable' function.
        // We need to make the function payable
        myNumber += y + msg.value;
    }
}

// We can define a library for explicitly converting ``address``
// to ``address payable`` as a workaround.
library AddressMakePayable {
    function makePayable(address x) internal pure returns (address payable) {
        return address(uint160(x));
    }
}

더 알아보기 (Learn more)