
Optimism 탈취 사건 심층 분석: Layer2 네트워크 스마트 계약 배포 리플레이 공격
사건 개요
올해 5월 말, Optimism 재단은 op 토큰의 유동성을 제공하기 위해 마켓 메이커인 Wintermute를 채용했다. 이 과정에서 Optimism 재단은 Wintermute 팀에 마켓 메이킹을 위한 2000만 개의 op 토큰을 제공했다. 그러나 이 과정에서 커뮤니케이션 오류가 발생했는데, Wintermute 팀이 Layer1(eth) 상의 수취 주소를 제공했지만 해당 주소는 아직 Layer2(Optimism) 상에 배포되지 않은 상태였다. Optimism 재단은 Layer2 계정으로 자금을 송금한 후, Wintermute 팀이 문제를 인지하게 되었고, 계정 복구 전에 공격자가 먼저 해당 계정 권한을 확보하여 계정 내 op 토큰을 매도하기 시작했다.
타임라인
● 05.26 및 05.27 - Optimism 재단은 Wintermute 팀이 제공한 주소 0x4f3a120e72c76c22ae802d129f599bfdbc31cb81로 각각 1 op와 100만 op를 송금하며 계정 테스트를 진행함.
● 05.27 - Optimism 재단이 해당 주소로 나머지 1900만 op를 송금함.
● 05.30 - Wintermute 팀이 계정 오류를 발견하고 Optimism 재단에 연락하며 Gnosis Safe 팀에 도움을 요청하여 자금 회수를 시도함. Wintermute 팀은 Optimism 재단과 Gnosis Safe 팀과 협의 후, 해당 계정은 여전히 안전하며 Wintermute 외에는 제어할 수 없으며 Gnosis Safe 팀의 지원 하에 자금을 회수할 수 있다고 판단하고, 06.07에 계정 권한을 복구할 계획을 세움.
● 06.01 - 공격자가 공격 스마트계약을 배포하며 팩토리(factory) 주소를 하드코딩함. 이는 공격자가 이미 전체 공격 절차를 확정지었다는 것을 의미함.
● 06.05 - 공격자가 공격을 실행하여 대상 계정의 권한을 확보하고, 100만 op를 tornado에 전송하여 환전함.
● 06.09 - Wintermute 팀이 이번 실수에 대해 전적으로 책임을 지겠다고 선언하며 공격자가 매도한 모든 토큰을 매입하겠다고 밝히고, 잔여 토큰 반환을 요구함. 성명 발표 4시간 후, 공격자는 다시 100만 op를 개인 계정으로 전송함.
공격 경로
1. 모든 Gnosis Safe 금고 계약은 Gnosis Safe 프록시 팩토리 계약에 의해 배포되며, 대상 주소의 제어권을 얻기 위해서는 프록시 팩토리 계약을 호출하여 해당 주소에 프록시 계약을 배포해야 함.
2. 공격 발생 전, Layer2(Optimism) 상에는 프록시 팩토리 계약이 아직 배포되지 않았음. 공격자는 Layer1의 팩토리 계약 배포 트랜잭션을 리플레이하여 Layer2 상에 새로운 팩토리 계약을 배포함.
3. Layer2 상에서 팩토리 계약의 createProxy 메서드를 여러 번 호출하여 프록시 계약을 배포하면서 팩토리 계약의 nonce를 계속 증가시키고, 최종적으로 대상 주소에 프록시를 배포함.
4. createProxy를 호출하여 프록시를 배포할 때, masterCopy 파라미터를 공격자가 제어하는 계약 주소로 설정함. masterCopy는 프록시의 implementation으로 사용됨. 이를 통해 공격자는 대상 주소의 제어권을 획득함.
이더리움 스마트계약 주소 생성 방식
위 공격 경로가 특정 주소에 계약을 배포할 수 있었던 이유를 설명하기 위해, 이더리움의 스마트계약 주소 생성 메커니즘을 이해할 필요가 있음.
스마트계약 주소는 대응하는 개인키가 없으며, 계약 배포 시점에 결정됨. 계약 배포 방법은 CREATE와 CREATE2 두 가지가 있음. CREATE 방식의 주소 생성 알고리즘은 배포를 시작한 주소와 해당 주소의 nonce 값을 RLP로 인코딩한 후 sha3 해시를 취하고, 마지막 20바이트를 새롭게 배포된 스마트계약의 주소로 사용함.
new_address = hash(sender, nonce)
자바스크립트 코드로 표현하면 다음과 같음:
const Web3 = require("web3");
const RLP = require("rlp");
const nonce = 0;
const account = "0xa990077c3205cbdf861e17fa532eeb069ce9ff96";
var e = RLP.encode(
[
account,
nonce,
],
);
const nonceHash = Web3.utils.sha3(Buffer.from(e));
console.log(nonceHash.substring(26));
EOA 계정의 경우, 트랜잭션을 하나 발행할 때마다 nonce 값이 +1 증가하며, 스마트계약 계정의 경우 새로운 계약을 생성할 때마다 nonce가 +1 증가함.
CREATE2는 일반적으로 스마트컨트랙트가 호출하며, 주소 생성 알고리즘은 다음과 같음. 인코딩 방식은 CREATE와 동일함:
new_address = hash(0xFF, sender, salt, bytecode)
● 0xff는 고정된 상수
● sender는 배포를 시작한 주소
● salt는 sender가 지정하는 임의의 값
● bytecode는 배포할 계약의 바이트코드
● CREATE2는 증가하는 nonce를 도입하지 않고, sender가 제어 가능한 salt로 대체함으로써 계약 배포 주소를 더 잘 제어할 수 있음.
기술적 세부 사항
Layer1 프록시 생성 과정
이전 섹션의 계약 주소 생성 방식에 따르면, Layer2의 특정 주소에 계약을 배포하려면 먼저 Layer1에서의 배포 방식을 확인해야 함. Layer1 상의 Wintermute 멀티시그 프록시는 트랜잭션 https://etherscan.io/tx/0xd705178d68551a6a6f65ca74363264b32150857a26dd62c27f3f96b8ec69ca01 에 의해 생성되었으며, 사용된 팩토리는 Proxy Factory 1.1.1임. 생성 방법은 createProxy임. ProxyFactory.createProxy 메서드의 구현은 다음과 같음:
function createProxy(address masterCopy, bytes memory data)
public
returns (Proxy proxy)
{
proxy = new Proxy(masterCopy);
if (data.length > 0)
// solium-disable-next-line security/no-inline-assembly
assembly {
if eq(call(gas, proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) { revert(0, 0) }
}
emit ProxyCreation(proxy);
}
여기서 new 키워드를 사용하여 Proxy 계약을 배포하고 있으며, new 키워드는底层에서 CREATE opcode를 호출하여 계약을 배포함. CREATE2가 아님.
따라서 Layer2 상에서 동일한 주소의 Proxy Factory 1.1.1 계약의 createProxy 메서드를 호출하면, 배포 시점에 동일한 nonce 값을 유지한다면 Layer1과 동일한 주소의 프록시 계약을 생성할 수 있음.
Layer2에서 Gnosis Safe 프록시 팩토리 1.1.1 리플레이 배포
공격 발생 전, Optimism 상에는 아직 Gnosis Safe 프록시 팩토리 1.1.1이 배포되지 않았음. 따라서 먼저 프록시 팩토리 계약을 Layer1과 동일한 주소에 배포해야 함.
Layer1 상의 프록시 팩토리 1.1.1 주소는 0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B이며, 트랜잭션 https://etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261 에 의해 2019년에 생성됨.
해당 트랜잭션을 디코딩함: https://www.ethereumdecoder.com/?search=0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261 , 원본 데이터는 다음과 같음:
{
"nonce": 2,
"gasPrice": {
"_hex": "0x02540be400"
},
"gasLimit": {
"_hex": "0x114343"
},
"to": "0x00",
"value": {
"_hex": "0x00"
},
"data": "xxxx...xxxx",
"v": 28,
"r": "0xc7841dea9284aeb34c2fb783843910adfdc057a37e92011676fddcc33c712926",
"s": "0x4e59ce12b6a06da8f7ec7c2d734787bd413c284fc3d1be3a70903ebc23945e8c"
}
v = 28임을 확인할 수 있으며, EIP-155에 따라 v = 28 또는 v = 27인 트랜잭션은 EIP-155 서명을 사용하지 않아 ChainID를 트랜잭션 서명에 포함하지 않음.
따라서 이 Layer1의 계약 배포 트랜잭션은 바로 Layer2(Optimism) 상에서 리플레이 가능함. 리플레이된 트랜잭션은 https://Optimism.etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261 이며, 이 거래로 새로운 Gnosis Safe 프록시 팩토리가 배포되었고, 주소는 Layer1과 동일함: https://Optimism.etherscan.io/address/0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b .

이미지에서 알 수 있듯이, 공격이 시작된 지 4일 전에야 계약이 생성되었으나, 생성 전부터 여러 차례의 계약 호출 기록이 존재함.
이 시점에서 팩토리 계약 계정의 nonce는 0이며, 스마트계약 계정의 경우 새로운 계약을 배포할 때마다 nonce가 +1 증가함. 따라서 createProxy 메서드를 계속 호출하여 프록시 계약을 배포함으로써 nonce를 누적시키면, 결국 목표 주소에 프록시 계약을 배포할 수 있음.
Layer2 대상 주소에 프록시 배포
공격 계약 0xe7145dd6287ae53326347f3a6694fcf2954bcd8a[1]
공격자는 이 공격 계약을 통해 ProxyFactory[2]의 createProxy 메서드를 대량 호출하며, masterCopy 주소 파라미터를 자신의 계약 주소로 설정함으로써 자신이 implementation인 다수의 프록시 계약을 생성함. 이를 통해 대상 계약을 제어함.
구체적인 과정:
공격자는 EOA 주소1[3]에서 여러 번 공격 계약을 호출하며, 각 트랜잭션은 162개의 Proxy 계약을 생성함. 최종적으로 트랜잭션 https://Optimism.etherscan.io/tx/0x00a3da68f0f6a69cb067f09c3f7e741a01636cbc27a84c603b468f65271d415b 에서 주소가 0x4f3a120e72c76c22ae802d129f599bfdbc31cb81인 대상 계약을 생성함.
공격자는 EOA 주소2[4]를 프록시 소유자(owner)로 하여 대상 계약을 제어함.
> eth_call 0x4f3a120E72C76c22ae802D129F599BFDbc31cb81 owner()
0x0000000000000000000000008bcfe4f1358e50a1db10025d731c8b3b17f04dbb
교훈
이더리움 레이어2 솔루션들(예: Optimism, Arbitrum)과 사이드체인들(xDai, Polygon 등)이 점점 더 많이 등장함에 따라 자산과 dapp들이 다양한 네트워크에 분산되어 있음. 그러나 EOA 계정과 Gnosis Safe 같은 스마트계약 계정은 크로스체인에서의 행동 양상이 매우 다름.
EOA 계정은 본질적으로 개인키 기반으로 네트워크를 넘어 사용 가능하지만, 스마트계약 계정은 자체적으로 개인키가 없으며 제어권과 기능을 확정하기 위해 복잡한 배포 및 초기화 로직이 필요하므로 직접적으로 크로스체인 사용이 불가능함.
또한 서로 다른 네트워크에서 인프라 역할을 하는 계약들도 무작정 존재하거나 동등하게 복제되는 것이 아님.
따라서 서로 다른 계층의 네트워크를 다룰 때, 계약 관련 작업은 신중해야 하며, 먼저 대상 네트워크와 이더리움 메인넷의 계약 주소, 내용, 상태가 동일한지 확인해야 함. 복잡한 로직이 관련될 경우, 대상 네트워크의 특성과 계약 기능 간 호환성까지 검토해야 함. Layer1의 주소가 무차별적으로 Layer2나 사이드체인에 매핑된다고 가정해서는 안 되며, Wintermute 팀의 탈취 사건과 같은 실수가 반복되지 않도록 주의해야 함.
참고 자료
[1] 공격 계약: https://Optimism.etherscan.io/address/0xe7145dd6287ae53326347f3a6694fcf2954bcd8a
[2] ProxyFactory: https://Optimism.etherscan.io/address/0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b#code
[3] 공격자 EOA 주소1: https://Optimism.etherscan.io/address/0x60B28637879B5a09D21B68040020FFbf7dbA5107
[4] 공격자 EOA 주소2: https://Optimism.etherscan.io/address/0x8bcfe4f1358e50a1db10025d731c8b3b17f04dbb
TechFlow 공식 커뮤니티에 오신 것을 환영합니다
Telegram 구독 그룹:https://t.me/TechFlowDaily
트위터 공식 계정:https://x.com/TechFlowPost
트위터 영어 계정:https://x.com/BlockFlow_News














