solidity 에서 call 과 delegatecall 의 작동방식 비교
- call 의 작동방식
- 호출자(caller)가 대상 컨트랙트(callee)의 코드를 실행하면서 대상 컨트랙트의 상태를 변경합니다.
- 핵심 특징
- 호출은 호출된 컨트랙트의 스토리지와 컨텍스트에서 수행됩니다.
- 호출자는 실행된 함수가 호출된 컨트랙트에서 어떤 상태도 변경하도록 허용합니다.
- msg.sender 는 호출자를 나타냅니다.
- 코드 예시
pragma solidity ^0.8.0;
contract Callee {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
contract Caller {
function callSetValue(address calleeAddress, uint256 _value) external {
(bool success, ) = calleeAddress.call(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success, "Call failed");
}
}
callSetValue 함수가 호출되면 Callee 컨트랙트의 value 상태 변수가 변경됩니다.
- delegatecall 의 작동방식
- 호출자(caller)가 대상 컨트랙트(callee)의 코드를 실행하되, 호출자의 상태와 컨텍스트를 사용합니다.
- 핵심 특징
- 호출된 함수는 호출자의 스토리지와 컨텍스트에서 실행됩니다.
- msg.sender는 원래의 호출자를 유지합니다.
- 대상 컨트랙트의 상태는 변경되지 않고, 호출자의 상태가 변경됩니다.
- 코드 예시
pragma solidity ^0.8.0;
contract Logic {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
contract Proxy {
uint256 public value;
function delegateSetValue(address logicAddress, uint256 _value) external {
(bool success, ) = logicAddress.delegatecall(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success, "Delegatecall failed");
}
}
delegateSetValue가 호출되면 Logic 컨트랙트의 코드가 실행되지만, Proxy 컨트랙트의 value 가 변경됩니다.
Delegatecall 작동 방식에 기반한 보안 고려사항
- 스토리지 충돌
- 문제: delegatecall 은 호출자의 스토리지에 영향을 미치므로, 호출자와 호출 대상의 스토리지 레이아웃이 일치하지 않으면 오류가 발생할 수 있습니다.
- 해결방법
- 엄격한 스토리지 레이아웃 관리.
- EIP-2535 Diamond Standard 와 같은 표준 사용.
- 코드 예시
- Logic 컨트랙트는 첫번째 슬롯에 값을 저장하려 하지만, Proxy 는 owner 를 첫번째 슬롯에 저장합니다. 이로 인해 owner 가 의도치 않게 변경될 수 있습니다.
// 취약한 코드
contract Logic {
uint256 public value; // 첫 번째 슬롯
}
contract Proxy {
address public owner; // 첫 번째 슬롯 - 충돌 발생
uint256 public value; // 두 번째 슬롯
function delegateSetValue(address logicAddress, uint256 _value) external {
(bool success, ) = logicAddress.delegatecall(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success, "Delegatecall failed");
}
}
- 신뢰할 수 없는 컨트랙트 호출
- 문제: 신뢰할 수 없는 컨트랙트를 대상으로 delegatecall 을 실행하면, 공격자가 호출자의 상태를 조작할 수 있습니다.
- 해결방법:
- 항상 신뢰할 수 있는 컨트랙트를 대상으로 호출.
- 주소를 변경할 수 있는 업그레이드 가능한 컨트랙트의 경우 엄격한 검증 절차를 추가.
- 코드 예시
// 취약한 코드
contract Proxy {
uint256 public value;
function delegateSetValue(address logicAddress, uint256 _value) external {
(bool success, ) = logicAddress.delegatecall(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success, "Delegatecall failed");
}
}
// 수정된 코드 : trustedLogic 변수를 사용해 신뢰할 수 있는 컨트랙트만 호출 가능하도록 제한.
contract Proxy {
address public trustedLogic;
uint256 public value;
constructor(address _logic) {
trustedLogic = _logic;
}
function delegateSetValue(uint256 _value) external {
(bool success, ) = trustedLogic.delegatecall(
abi.encodeWithSignature("setValue(uint256)", _value)
);
require(success, "Delegatecall failed");
}
}
Delegatecall의 보안 요약
1. 스토리지 레이아웃 일치: 호출자와 호출 대상의 상태 변수 배치가 일치하도록 설계.
2. 신뢰할 수 있는 주소만 호출: delegatecall 대상이 신뢰할 수 있는 컨트랙트인지 확인.
3. 재진입 방지: 상태 업데이트를 먼저 수행하거나, ReentrancyGuard 사용.
4. 정확한 업그레이드 로직 관리: 업그레이드 가능한 컨트랙트의 경우 권한 제어와 주소 검증 필요.
레퍼런스
• Solidity Documentation - delegatecall
'Solidity' 카테고리의 다른 글
| Ethereum 에서 사용할 수 있는 주요 오라클 서비스 (2) | 2025.01.03 |
|---|---|
| 외부 오라클을 사용한 난수 생성 방법 (5) | 2025.01.03 |
| Solidity에서의 예외처리: require vs. assert (0) | 2025.01.02 |
| OpenZeppelin PullPayment 의 사용 사례 (0) | 2025.01.02 |
| 스마트 컨트랙트 개발 시의 보안 취약점 (1) | 2025.01.02 |