CALL은 대상 주소의 코드와 storage·balance 문맥에서 실행하며 대상이 보는 msg.sender는 호출 계약입니다. DELEGATECALL은 대상의 코드만 빌리고 호출자의 주소·storage·balance에서 실행하며 원래 msg.sender와 msg.value를 유지합니다. STATICCALL은 CALL과 비슷하지만 value 인수가 없고 현재 호출과 하위 호출에 static flag를 전달해 SSTORE, LOG, CREATE와 value 전송 같은 상태 변경을 예외로 막습니다. 세 opcode 모두 성공 여부와 returndata를 반환하므로 low-level call 결과를 확인하지 않으면 실패를 성공처럼 처리할 수 있습니다.
코드 주소와 상태 주소를 먼저 나눕니다
Contract A가 B를 CALL하면 EVM은 B 주소의 bytecode를 B 계정 문맥에서 실행합니다. B의 SLOAD·SSTORE는 B storage를 읽고 쓰며 ADDRESS는 B, BALANCE 관련 문맥도 B를 가리킵니다. B가 보는 msg.sender는 A이고 CALL에 value가 있으면 msg.value로 전달됩니다. 외부 사용자가 A를 호출했다는 사실은 B에서 msg.sender만 보고 직접 알 수 없습니다.
DELEGATECALL은 B의 bytecode를 가져오지만 실행 주소는 A로 유지합니다. 따라서 B 코드의 slot 0 접근은 B가 아니라 A slot 0을 건드립니다. ADDRESS는 A이고 원래 외부 호출의 msg.sender와 msg.value가 보존됩니다. 이 특성이 프록시 구현을 가능하게 하지만 구현 계약의 변수 순서가 프록시 상태와 다르면 멀쩡한 코드가 관리자나 잔액을 덮어쓸 수 있습니다.
DELEGATECALL은 다른 계약의 상태로 이동하는 호출이 아니라, 다른 주소의 코드를 현재 계약의 상태 위에서 실행하는 방식입니다.
JOBCOIN 해설

그림은 이 주제의 공통 개념을 단순화한 설명입니다. 아래 항목에서 이 글의 구체적인 조건과 예외를 함께 읽어보세요.
- 코드 주소와 상태 주소를 먼저 나눕니다
Contract A가 B를 CALL하면 EVM은 B 주소의 bytecode를 B 계정 문맥에서 실행합니다.
- 세 opcode의 관찰 문맥을 표로 비교합니다
STATICCALL은 대상 코드를 별도 대상 문맥에서 읽되 static flag를 true로 둡니다.
- 프록시 한 번의 호출을 단계별로 추적합니다
사용자 U가 Proxy P 의 transfer 함수를 부르면 P fallback은 calldata를 Implementation I에 DELEGATECALL합니다.
AI로 제작한 개념도 · 실제 가격·거래 내역·통계가 아닙니다.
세 opcode의 관찰 문맥을 표로 비교합니다
STATICCALL은 대상 코드를 별도 대상 문맥에서 읽되 static flag를 true로 둡니다. EIP-214는 SSTORE, CREATE·CREATE2, LOG0부터 LOG4, SELFDESTRUCT와 0이 아닌 value의 CALL을 상태 변경으로 보고 예외를 내도록 정합니다. 호출된 코드가 다시 다른 계약을 호출해도 static flag가 복사되므로 우회해서 저장할 수 없습니다.
view 함수라는 Solidity 표시는 compiler가 STATICCALL을 선택하는 데 도움을 주지만, 임의 bytecode의 의미를 인간이 신뢰하라는 보증이 아닙니다. Static 문맥에서는 로그조차 상태 변경으로 금지되며, 호출 실패는 바깥 transaction 전체를 자동으로 revert시키지 않고 low-level 성공 값으로 돌아올 수 있습니다.
| 항목 | CALL | DELEGATECALL | STATICCALL |
|---|---|---|---|
| 실행 코드 | 대상 | 대상 | 대상 |
| storage 주소 | 대상 | 호출자 | 대상 |
| msg.sender | 호출자 계약 | 원래 sender 유지 | 호출자 계약 |
| msg.value | 지정 value | 원래 value 유지 | 0 |
| 상태 변경 | 가능 | 호출자 상태에 가능 | 현재·하위 frame 금지 |
프록시 한 번의 호출을 단계별로 추적합니다
사용자 U가 Proxy P의 transfer 함수를 부르면 P fallback은 calldata를 Implementation I에 DELEGATECALL합니다. I 코드가 balances[msg.sender]를 읽을 때 msg.sender는 U이고 storage는 P입니다. I 주소에 직접 저장된 balances를 읽지 않습니다. I 코드가 address(this)를 검사하면 I가 아니라 P가 나옵니다.
반대로 P가 I를 CALL했다면 I의 storage를 읽고 msg.sender는 P가 됩니다. 권한 검사 onlyOwner가 사용자 U를 기대했다면 실패하거나 P 자체가 권한자로 취급될 수 있습니다. 호출 종류 한 단어가 인증과 자산 위치를 동시에 바꾸므로 trace에는 opcode, code address, context address, sender, value를 함께 남깁니다.
- Proxy와 implementation의 storage slot 표를 비교합니다.
- Trace에서 CALL 계열 opcode와 depth를 확인합니다.
- 각 frame의 msg.sender·msg.value·address(this)를 기록합니다.
- Success bit와 returndata를 ABI 기준으로 디코딩합니다.
- 업그레이드 전 delegatecall 회귀 테스트를 실행합니다.
실패와 returndata를 분리해 처리합니다
CALL 계열 opcode는 callee가 revert하거나 out of gas가 나면 success 0과 returndata를 돌려줍니다. Solidity high-level external call은 보통 예외를 전파하지만 address.call 같은 low-level API는 boolean을 확인해야 합니다. 반환값을 버리면 토큰 전송이나 관리자 호출 실패 뒤에도 로컬 로직이 계속될 수 있습니다.
Returndata가 Error(string), Panic(uint256), custom error 형식인지 또는 빈 bytes인지 구분합니다. 공격 대상은 임의 bytes를 정상 ABI 오류처럼 만들 수 있으므로 selector만 보고 신뢰하지 않습니다. Gas 전달도 EIP-150의 63/64 규칙과 callee 작업량에 영향을 받으며 gas가 많다는 이유로 호출 안전성이 생기지 않습니다.
STATICCALL도 읽기 결과의 최신성을 보장하지 않습니다
Static 실행은 해당 frame 동안 state write를 막을 뿐 조회한 block state가 최종 확정됐다는 의미가 아닙니다. eth_call의 block tag가 latest이면 reorg로 결과가 달라질 수 있고, 함수는 timestamp·block number·다른 계약 상태에 따라 매번 다른 값을 반환할 수 있습니다.
진단에서는 opcode 수준의 쓰기 금지와 RPC 조회 기준을 분리합니다. 안전한 프록시는 implementation code hash, upgrade authority, storage layout을 검증하고, 읽기 서비스는 safe 또는 finalized block 기준과 실패 반환을 기록해야 합니다.
자주 묻는 질문
DELEGATECALL 대상이 msg.sender를 프록시로 보나요?
아닙니다. DELEGATECALL은 호출 전 frame의 msg.sender와 msg.value를 보존하므로 일반 프록시에서는 원래 사용자를 봅니다.
STATICCALL 안에서 event를 emit할 수 있나요?
없습니다. LOG opcode는 EIP-214의 상태 변경 금지 목록에 있어 static 문맥에서 예외가 납니다.
Low-level call의 false는 transaction 전체 revert인가요?
호출 frame 실패를 뜻합니다. 호출자가 false를 확인해 직접 revert하지 않으면 바깥 실행은 계속될 수 있습니다.
더 깊이 읽기
본문에서 다룬 개념과 확인 절차를 다음 글에서 이어서 살펴보세요.



