Solidity v0.8.0 Breaking Changes

Solidity v0.8.0 Breaking Changes

이 절은 Solidity 버전 0.8.0에서 도입된 주요 breaking change를 강조해요. 전체 목록은 릴리스 changelog를 확인해요. 0.8.0은 산술 오버플로우·언더플로우 자동 체크, ABI coder v2 기본 활성화, 명시적 형변환 제한 등 개발자 경험에 크게 영향을 주는 변경이 많아요.

출처: 문서

본문

이 절은 Solidity 버전 0.8.0에서 도입된 주요 breaking change를 강조해요. 전체 목록은 릴리스 changelog를 확인해요.

의미의 조용한 변경 (Silent Changes of the Semantics)

이 절은 컴파일러가 알려주지 않고 기존 코드의 동작이 바뀌는 변경을 나열해요.

  • 산술 연산은 언더플로우와 오버플로우에서 revert해요. 이전의 래핑 동작을 사용하려면 unchecked { ... }를 사용할 수 있어요. 오버플로우 검사는 매우 흔하므로 가스 비용이 약간 늘어나더라도 가독성을 높이기 위해 기본값으로 만들었어요.
  • ABI coder v2가 기본으로 활성화돼요. pragma abicoder v1;을 사용해 이전 동작을 선택할 수 있어요. pragma experimental ABIEncoderV2; 프라그마는 여전히 유효하지만 비권장이고 효과가 없어요. 명시적으로 하고 싶다면 pragma abicoder v2;를 사용해요. ABI coder v2는 v1보다 더 많은 타입을 지원하고 입력에 더 많은 정합성 검사를 수행해요. ABI coder v2는 일부 함수 호출을 더 비싸게 만들고, 파라미터 타입에 맞지 않는 데이터를 포함할 때 ABI coder v1로는 revert하지 않던 컨트랙트 호출도 revert하게 만들 수 있어요.
  • 지수화는 오른쪽 결합이에요. 즉 a**b**c는 a**(b**c)로 파싱돼요. 0.8.0 이전에는 (a**b)**c로 파싱됐어요. 이것이 지수화 연산자를 파싱하는 흔한 방식이에요.
  • 실패한 어서션과 0으로 나누기나 산술 오버플로우 같은 다른 내부 검사는 invalid opcode가 아니라 revert opcode를 사용해요. 더 구체적으로, 상황에 특화된 에러 코드를 가진 Panic(uint256) 함수 호출과 같은 에러 데이터를 사용해요. 이는 에러에서 가스를 절약하면서도 정적 분석 도구가 이런 상황을 실패한 require 같은 잘못된 입력의 revert와 구분할 수 있게 해줘요.
  • 길이가 잘못 인코딩된 스토리지의 바이트 배열에 접근하면 panic이 일어나요. 인라인 어셈블리로 스토리지 바이트 배열의 원시 표현을 수정하지 않으면 컨트랙트는 이 상황에 빠질 수 없어요.
  • 배열 길이 표현식에 상수가 사용되면, 이전 Solidity 버전은 평가 트리의 모든 분기에서 임의 정밀도를 사용했어요. 이제 상수 변수가 중간 표현식으로 사용되면, 그 값은 런타임 표현식에서 사용될 때와 같은 방식으로 제대로 반올림돼요.
  • byte 타입이 제거됐어요. bytes1의 별칭이었어요.

새로운 제한 (New Restrictions)

이 절은 기존 컨트랙트가 더 이상 컴파일되지 않게 만들 수 있는 변경을 나열해요.

  • 리터럴의 명시적 변환과 관련된 새로운 제한이 있어요. 이전 동작은 다음 경우에 모호했을 가능성이 높아요:
    • 음수 리터럴과 type(uint160).max보다 큰 리터럴에서 address로의 명시적 변환을 허용하지 않아요.
    • 리터럴과 정수 타입 T 사이의 명시적 변환은 리터럴이 type(T).min과 type(T).max 사이에 있을 때만 허용돼요. 특히 uint(-1)의 사용은 type(uint).max로 교체해요.
    • 리터럴과 enum 사이의 명시적 변환은 리터럴이 enum의 값을 나타낼 수 있을 때만 허용돼요.
    • 리터럴과 address 타입 사이의 명시적 변환(예: address(literal))은 address payable이 아니라 address 타입을 가져요. 명시적 변환, 즉 payable(literal)을 사용해 payable 주소 타입을 얻을 수 있어요.
  • 주소 리터럴은 address payable이 아니라 address 타입을 가져요. 명시적 변환, 예: payable(0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF)을 사용해 address payable로 변환할 수 있어요.
  • 명시적 타입 변환에 새로운 제한이 있어요. 부호, 너비, 타입 범주(int, address, bytesNN 등) 중 최대 하나만 바뀔 때만 변환이 허용돼요. 여러 변경을 수행하려면 여러 변환을 사용해요. T(S) 표기로 T(x) 명시적 변환을 나타내자고 해요. 여기서 T와 S는 타입이고, x는 S 타입의 임의 변수예요. 그러한 허용되지 않는 변환의 예는 uint16(int8)이에요. 너비(8비트에서 16비트)와 부호(부호 있는 정수에서 부호 없는 정수)를 둘 다 바꾸기 때문이에요. 변환을 하려면 중간 타입을 거쳐야 해요. 이전 예시에서 이는 uint16(uint8(int8)) 또는 uint16(int16(int8))이에요. 두 방법은 예를 들어 -1에 대해 다른 결과를 만든다는 점에 주의해요. 이 규칙으로 허용되지 않는 변환의 몇 가지 예는 다음과 같아요:
    • address(uint)와 uint(address): 타입 범주와 너비를 둘 다 변환. 각각 address(uint160(uint))와 uint(uint160(address))로 교체.
    • payable(uint160), payable(bytes20), payable(integer-literal): 타입 범주와 상태 가변성을 둘 다 변환. 각각 payable(address(uint160)), payable(address(bytes20)), payable(address(integer-literal))로 교체. payable(0)은 유효하고 이 규칙의 예외라는 점에 주의.
    • int80(bytes10)과 bytes10(int80): 타입 범주와 부호를 둘 다 변환. 각각 int80(uint80(bytes10))과 bytes10(uint80(int80))로 교체.
    • Contract(uint): 타입 범주와 너비를 둘 다 변환. Contract(address(uint160(uint)))로 교체.
    • 이러한 변환은 모호함을 피하려고 허용되지 않았어요. 예를 들어 표현식 uint16 x = uint16(int8(-1))에서 x의 값은 부호 또는 너비 변환이 먼저 적용되는지에 따라 달라질 거예요.
  • 함수 호출 옵션은 한 번만 줄 수 있어요. 즉 c.f{gas: 10000}{value: 1}()은 유효하지 않고 c.f{gas: 10000, value: 1}()로 바꿔야 해요.
  • 전역 함수 log0, log1, log2, log3, log4가 제거됐어요. 대부분 사용되지 않던 저수준 함수들이에요. 그 동작은 인라인 어셈블리에서 접근할 수 있어요.
  • enum 정의는 256개를 초과하는 멤버를 포함할 수 없어요. 이렇게 하면 ABI의 기본 타입이 항상 uint8이라고 안전하게 가정할 수 있어요.
  • this, super, _ 이름의 선언을 허용하지 않아요. public 함수와 이벤트는 예외예요. 이 예외는 Solidity가 아닌 언어로 구현돼 그런 함수 이름을 허용하는 컨트랙트의 인터페이스를 선언할 수 있게 하기 위한 것이에요.
  • 코드에서 \b, \f, \v 이스케이프 시퀀스 지원을 제거해요. 각각 \x08, \x0c, \x0b 같은 16진수 이스케이프를 통해 여전히 삽입할 수 있어요.
  • 전역 변수 tx.origin과 msg.sender는 address payable이 아니라 address 타입을 가져요. 명시적 변환, 즉 payable(tx.origin) 또는 payable(msg.sender)을 사용해 address payable로 변환할 수 있어요. 이 변경은 컴파일러가 이 주소들이 payable인지 여부를 결정할 수 없기 때문에, 요구 사항을 보이게 명시적 변환을 요구하기 위해 수행됐어요.
  • address 타입으로의 명시적 변환은 항상 non-payable 주소 타입을 반환해요. 특히 다음 명시적 변환은 address payable이 아니라 address 타입을 가져요:
    • address(u) — u는 uint160 타입 변수. 두 번의 명시적 변환, 즉 payable(address(u))로 u를 address payable 타입으로 변환할 수 있어요.
    • address(b) — b는 bytes20 타입 변수. 두 번의 명시적 변환, 즉 payable(address(b))로 b를 address payable 타입으로 변환할 수 있어요.
    • address(c) — c는 컨트랙트. 이전에는 이 변환의 반환 타입이 컨트랙트가 Ether를 받을 수 있는지(receive 함수나 payable fallback 함수를 가졌는지)에 따라 달랐어요. 변환 payable(c)은 address payable 타입을 가지며 컨트랙트 c가 Ether를 받을 수 있을 때만 허용돼요. 일반적으로 다음 명시적 변환으로 c를 항상 address payable 타입으로 변환할 수 있어요: payable(address(c)). address(this)는 address(c)와 같은 범주에 해당하고 같은 규칙이 적용돼요.
  • 인라인 어셈블리의 chainid 내장 함수는 이제 pure가 아니라 view로 간주돼요.
  • 단항 부정은 더 이상 부호 없는 정수에 사용할 수 없고, 부호 있는 정수에만 사용할 수 있어요.

인터페이스 변경 (Interface Changes)

  • --combined-json의 출력이 바뀌었어요. JSON 필드 abi, devdoc, userdoc, storage-layout은 이제 하위 객체예요. 0.8.0 이전에는 문자열로 직렬화됐어요.
  • "legacy AST"가 제거됐어요(명령줄 인터페이스의 --ast-json과 표준 JSON의 legacyAST). 대신 "compact AST"(각각 --ast-compact-json / AST)를 사용해요.
  • 옛 에러 리포터(--old-reporter)가 제거됐어요.

코드를 업데이트하는 방법 (How to update your code)

  • 래핑 산술에 의존한다면 각 연산을 unchecked { ... }로 감싸요.
  • 선택 사항: SafeMath나 비슷한 라이브러리를 사용한다면 x.add(y)를 x + y로, x.mul(y)를 x * y 등으로 바꿔요.
  • 옛 ABI coder를 유지하고 싶다면 pragma abicoder v1;을 추가해요.
  • 선택 사항: 중복이므로 pragma experimental ABIEncoderV2 또는 pragma abicoder v2를 제거해요.
  • byte를 bytes1로 바꿔요.
  • 필요하면 중간 명시적 타입 변환을 추가해요.
  • c.f{gas: 10000}{value: 1}()을 c.f{gas: 10000, value: 1}()로 결합해요.
  • msg.sender.transfer(x)를 payable(msg.sender).transfer(x)로 바꾸거나 address payable 타입의 저장 변수를 사용해요.
  • x**y**z를 (x**y)**z로 바꿔요.
  • log0, …, log4의 대체로 인라인 어셈블리를 사용해요.
  • 부호 없는 정수를 타입의 최댓값에서 빼고 1을 더해 부정해요(예: type(uint256).max - x + 1, x가 0이 아닌지 확인하면서).

더 알아보기 (Learn more)