CREATE2는 정말 문제가 없을까?

EIP 1014

Constantinople hard fork에 대한 이미지 검색결과
https://medium.com/@Fota/ethereum-constantinople-hard-fork-profit-opportunity-part-1-b9394ad88cbb

이번 콘스탄티노플 하드포크 스펙에 포함된 EIP 중 EIP 1283re-entrancy 문제로 인해 하드포크 스펙에서 제외되었습니다. 이후 콘스탄티노플 하드포크 스펙에 포함된 EIP 1014 즉, CREATE2 opcode에 대한 이슈가 새로 제기되었습니다. 그 이슈에 대한 논의는 여기서 찾아볼 수 있습니다. 요약하자면 CREATE2로 인해 유저가 사용하고 있던 기존의 컨트랙트가 새로운 컨트랙트로 대체될 수 있다는 것입니다. 즉 같은 주소를 가지지만 다른 기능을 가진 컨트랙트로 대체될 수 있다는 것입니다.

55차 이더리움 개발자 회의에서 이와 같은 문제가 CREATE2로 인한 특정한 공격 패턴으로 발생할 수 있음을 인식하고 있습니다. 하지만 CREATE2 자체의 결함 때문은 아니며, CREATE2는 꼭 필요한 기능이기 때문에 콘스탄티노플 스펙에서 제외하지 않을 것이라고 결론지었습니다.

EIP 1014에서 제기된 re-entrancy 문제는 콘스탄티노플 이전에 작성된 컨트랙트에 re-entrancy 공격을 감행할 수 있다는 점 때문에 이번 하드포크 스펙에서 제외한 것 같습니다. 반면 EIP 1283에서 생기는 공격 패턴은 콘스탄티노플 하드포크 이전에 생성된 컨트랙트에 적용할 수 없습니다. 그렇기 때문에 EIP 1014 스펙은 이번 콘스탄티노플 하드포크 스펙에서 제외되지 않았습니다. 하지만 콘스탄티노플 하드포크 이후 이러한 공격 패턴이 존재할 수 있음을 인식하는 것이 중요하고, 이에 따라 컨트랙트 코드 오딧팅 범위도 그만큼 늘어나게 될 것입니다.

앞으로의 내용은 EIP 1014, 즉 CREATE2에 대한 내용과 CREATE2로 인한 공격 패턴이 어떻게 발생하는 지에 대한 것입니다.

EIP 1014

우선 EIP 1014에 대해서 살펴보겠습니다.

Specifications

EIP 1014는 새로운 CREATE2 opcode를 0xf5에 추가합니다. opcode CREATE2는 기존의 opcode CREATE와 마찬가지로 컨트랙트를 생성합니다. 하지만 CREATE2는 CREATE와 다른 방식으로 컨트랙트 주소를 만듭니다.

CREATE는 msg.sender의 address와 msg.sender의 nonce를 이용해서 컨트랙트 주소를 만듭니다.

keccak256(rlp([sender, nonce]))[12:]

CREATE2는 컨트랙트 주소를 만들 때 4개의 값(0xff, address, salt, init code)을 이용합니다.

keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
make contract address by CREATE & CREATE2 (go-ethereum)

여기서 중요한 차이점은 CREATE는 nonce를 사용하지만 CREATE2는 nonce를 사용하지 않습니다. 이것이 중요한 이유는 CREATE로 같은 주소의 컨트랙트를 새로 만들 수 없지만 CREATE2를 이용하면 같은 주소의 컨트랙트를 새로 만들 수 있기 때문입니다.

CREATE는 nonce를 이용해 주소를 만듭니다. 이 nonce는 메시지 콜을 하거나 컨트랙트를 생성할 때마다 1 증가하기 때문에 CREATE로 컨트랙트 주소를 만들면 항상 다른 주소를 생성합니다. 이에 반해 CREATE2는 nonce를 사용하지 않고 0xff, address, salt, init code을 이용해 컨트랙트 주소를 생성합니다. 그렇기 때문에 address, salt, init code를 미리 알고 있다면 같은 주소의 컨트랙트를 재배포할 수 있습니다.

CREATE2를 이용해서 같은 주소의 컨트랙트를 두 번 이상 배포할 수 있다는 의미는 아닙니다. CREATE 또는 CREATE2로 생성된 컨트랙트는 nonce가 1로 set됩니다. 컨트랙트를 생성할 때 해당 주소의 nonce가 0인지 검사하고 0이 아니면 revert합니다. 그렇기 때문에 이미 한번 배포된 컨트랙트는 같은 주소로 재배포될 수 없습니다. 단 해당 컨트랙트가 selfdesturct 함수 호출로 블록체인에서 삭제된 경우에는 CREATE2를 이용해 삭제된 컨트랙트와 같은 주소로 컨트랙트를 재배포할 수 있습니다.

따라서 컨트랙트의 상태를 다음과 같이 정의할 수 있습니다.

  • 콘스탄티노플 하드포크 이전
    : “not yet deployed”, “deployed”, or “self-destructed”.
  • 콘스탄티노플 하드포크 이후
    : “not yet deployed”, “deployed”, “self-destructed”, or “redeployed”

Rationale

  • Address formula
    : 기존에 CREATE로 만들어진 컨트랙트 주소와 충돌하지 않기 위해서 CREATE2는 컨트랙트 주소를 만들 때 0xff를 prefix로 사용합니다. CREATE는 컨트랙트 주소를 만들 때 rlp 인코딩을 하지만 CREATE2는 rlp 인코딩을 하지 않습니다. 우선 55 bytes 이상 길이의 데이터를 rlp 인코딩하면 그 결과 값의 prefix는 데이터의 길이에 따라 [0xf8, 0xff]의 range를 가집니다. CREATE에서 컨트랙트 주소를 만들 때 사용하는 address는 20 bytes이고 nonce는 8 bytes이기 때문에 55 bytes보다 커질 수 없습니다. 즉 address와 nonce를 rlp 인코딩한 값의 prefix는 0xff가 될 수 없습니다. 그렇기 때문에 기존 CREATE로 만들어진 주소와 CREATE2로 만들어질 주소는 충돌하지 않습니다.
  • Gas cost
    : CREATE2는 init code를 hash한 값을 컨트랙트 주소를 생성할 때 사용합니다. 만약 큰 길이의 init code를 이용해서 반복적으로 CREATE2를 사용하게 되면 DoS 공격의 위험이 있습니다. 그렇기 때문에 init code의 길이에 따라 가스를 추가로 소모합니다. CREATE2는 컨트랙트를 생성하기 때문에 컨트랙트 생성에 필요한 가스 32,000 gas를 소모하고 init code의 길이에 따라 1 word(32 bytes)당 6 gas를 소모합니다.

Motivation

EIP 1014가 도입된 동기는 state channel에서 언급한 Counterfactual가 가능하도록 하기 위함입니다.

Counterfactual X라는 개념은 다음과 같습니다.

“Counterfactual X”

  1. X could happen on-chain, but doesn’t
  2. The enforcement mechanism allows any participant to unilaterally make X happen
  3. Participants can act as though X has happened

CREATE2로 Counterfactual X가 가능해집니다. CREATE2로 만들어지는 컨트랙트 주소는 미리 예측할 수 있습니다. 컨트랙트를 먼저 배포하고 합의를 하는 것이 아니라 배포될 컨트랙트 주소에 합의를 합니다. 마치 컨트랙트가 실제로 on-chain 상에 배포되진 않았지만 배포된 것처럼 행동할 수 있습니다. 이것이 가능한 이유는 컨트랙트 주소에 합의가 이루어졌다면 이후 enforcement mechanism, 즉 강제 매커니즘에 의해 해당 주소로 컨트랙트를 만들기 때문입니다.

이에 대한 자세한 내용은 state channel 블로그 또는 온더 세미나 영상 (State Channel Basic — 박주형(Carl, Onther Inc.))에서 참고하시길 바랍니다.

CREATE2 취약점

CREATE2로 인해 같은 주소로 컨트랙트를 재배포할 수 있습니다. 같은 주소로 컨트랙트를 재배포하기 위해서는 해당 주소의 컨트랙트가 selfdestruct되어야 합니다.

이로 인해 두 가지 문제가 발생할 수 있습니다. 하나는 init code의 indeterminacy 문제이고 또 다른 하나는 CREATE와 CREATE2를 같이 사용할 때 재배포 문제입니다.

init code의 indeterminacy 문제

CREATE2는 컨트랙트 주소를 만들 때 init code를 사용합니다. 즉 init code를 해시한 값을 이용해 컨트랙트 주소를 생성합니다. 만약 init code가 다르다면 CREATE2로 만들어지는 컨트랙트 주소도 달라지겠죠. 하지만 문제는 init code 자체가 indeterminacy하다면 문제가 될 수 있습니다.

여기서 말하는 init code가 indeterminacy하다라는 의미는 init code 자체에 DELEGATECALL이나 CALLCODE가 포함되어 예상과 다른 상태를 가질 수 있음을 말합니다. 그렇기 때문에 해당 컨트랙트를 selfdestruct를 한 다음 재배포할 때 init code가 같음에도 불구하고 기존의 컨트랙트 상태와 다른 상태를 가질 수 있는 문제를 가집니다.

CREATE와 CREATE2를 같이 사용할 때 재배포 문제

CREATE2가 도입되면 CREATE에 새로운 문제가 발생합니다.

이 문제는 1) CREATE로 생성된 컨트랙트의 부모 컨트랙트가 CREATE2로 만들어진 컨트랙트일 때 그리고 2) 이 두 개의 컨트랙트 모두 destructible할 때 발생합니다.

여기서 컨트랙트가 destructible하다는 의미는 selfdestruct 함수 호출로 인해 컨트랙트가 삭제될 수 있다는 의미입니다.

아래는 예시 코드입니다.

A contract (created by CREATE2)

A 컨트랙트는 CREATE2로 만들어진 컨트랙트입니다(CREATE2로 만들어졌다고 가정). B 컨트랙트는 A 컨트랙트의 deployB 함수 호출을 통해 만들어집니다. 즉 A 컨트랙트는 B 컨트랙트의 부모 컨트랙트 입니다.

new 키워드는 opcode CREATE를 사용해서 컨트랙트를 생성합니다.

이 때 A 컨트랙트와 B 컨트랙트는 다음과 같은 address와 nonce를 가집니다(컨트랙트가 새로 배포되면 nonce는 1로 set됩니다.).

  • A 컨트랙트: address: 0xdead…,nonce: 1
  • B 컨트랙트: address: 0xbeef…, nonce: 1

만약 A 컨트랙트와 B 컨트랙트 모두 destructible하다면 유저는 이 두 개의 컨트랙트를 삭제할 수 있습니다. A 컨트랙트와 B 컨트랙트 모두 selfdestruct를 호출해서 삭제합니다. 그렇게 되면 두 컨트랙트의 상태는 블록체인 상에 남아있지 않게 됩니다. 즉 컨트랙트의 nonce가 0으로 set됩니다.

이후 A 컨트랙트를 CRAETE2로 재배포합니다. CREATE2를 이용해서 0xdead… 주소, 즉 이전 컨트랙트 주소와 같은 주소로 컨트랙트를 만들 수 있습니다. 그렇게 되면 A 컨트랙트의 address와 nonce는 이전과 같은 address와 nonce를 가집니다.

  • A 컨트랙트: address: 0xdead…, nonce: 1

이후 A 컨트랙트에서 deployB 함수가 아닌 deployC라는 함수를 호출해서 C 컨트랙트를 배포합니다. 여기서 문제는 B가 아닌 C 컨트랙트 배포했음에도 불구하고 address가 같다는 것입니다. 왜냐하면 CREATE를 이용해서 C 컨트랙트를 배포할 때 A 컨트랙트의 address와 nonce를 이용하기 때문입니다. 즉 B 컨트랙트를 만들 때 사용한 A 컨트랙트의 address와 nonce를 C 컨트랙트를 만들 때도 똑같이 사용하게 됩니다.

  • C 컨트랙트: address: 0xbeef…, nonce: 1

따라서 이러한 방식으로 같은 주소이지만 B 컨트랙트가 아닌 C 컨트랙트로 배포가 가능합니다. 즉 다른 기능을 하는 컨트랙트를 같은 주소로 재배포할 수 있게 됩니다.

공격 시나리오

이러한 점을 이용해 아래와 같은 공격 시나리오가 가능해집니다.

먼저 아래의 컨트랙트는 OMG 토큰과 DAI 토큰을 1:1 교환해주는 서비스 컨트랙트입니다.

공격 시나리오는 다음과 같습니다.

  1. OMGVendor 컨트랙트를 배포합니다. 이 때 부모 컨트랙트는 CREATE2로 만들어진 컨트랙트이며 부모 컨트랙트는 destructible합니다. OMGVender 컨트랙트 또한 destructible합니다.
  2. 유저는 해당 서비스를 사용하기 위해 해당 컨트랙트 주소로 컨트랙트 코드를 확인합니다.
  3. 코드의 문제점이 없음을 확인하고 유저는 OMG 토큰을 DAI 토큰으로 교환하기 위해 OMGVendor 컨트랙트 주소로 자신의 OMG 토큰을 approve합니다.
  4. 이후 공격자는 1) 부모 컨트랙트와 OMGVendor 컨트랙트를 destruct하고 2) 두 개의 컨트랙트를 다시 재배포합니다. 이 때 OMGVendor 컨트랙트와 같은 주소이지만 approve된 OMG 토큰을 공격자 계정으로 transferFrom하는 기능을 가진 컨트랙트로 재배포합니다. 그렇게 되면 유저는 자신이 approve한 OMG 토큰을 모두 잃게 됩니다.

예방책

이러한 문제를 예방하기 위해서는 다음과 같은 방식을 취할 수 있습니다.

  • destructible한 컨트랙트와 interaction하지 않는다. 이것이 가장 위의 문제를 막을 수 있는 확실한 방법인 것 같습니다.
  • 트랜잭션 배포 히스토리를 검증한다. 이는 부모 컨트랙트가 CREATE2로 생성된 컨트랙트인지 확인하는 방법이 될 것입니다.
  • 컨트랙트를 call하기 전에 컨트랙트의 EXTCODEHASH를 확인한다. EXTCODEHASH는 init code의 hash 값입니다. 그렇기 때문에 만약 EXTCODEHASH 값이 다르다면 기능이 다른 컨트랙트가 재배포된 것이라고 볼 수 있습니다. 하지만 이 방법은 init code가 같더라도 init code 자체가 indeterminacy하다면 효율적이지 않을 수 있습니다. 그렇기 때문에 CREATE2를 쓸 때 init code를 검증하는 것이 컨트랙트 코드를 verify하는 것만큼 중요합니다.

마치며

CREATE2는 이러한 문제점 이외에 여러 장점을 가집니다. 우선 기존에 사용하던 CREATE에서는 컨트랙트 코드와 컨트랙트 주소의 연관성이 없기 때문에 컨트랙트 주소 자체에 identity가 없습니다. 하지만 CREATE2는 컨트랙트 코드를 이용해 컨트랙트 주소를 만들기 때문에 컨트랙트 주소 자체에 어느 정도 identity를 가진다고 볼 수 있습니다. 또한 L2 솔루션에서 CREATE2를 이용해 Counterfatual와 같은 다양한 기술적 접근이 가능합니다.

CREATE2에 대한 견해를 잘 정리한 트윗을 링크로 남기며 글을 마치도록 하겠습니다. 혹시 궁금한 내용 또는 잘못된 내용이 있다면 댓글 또는 메일 보내주시면 감사하겠습니다.

Reference