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이 아닌지 확인하면서).