solidity 에서 call 과 delegatecall 의 작동방식 비교

  1. 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 상태 변수가 변경됩니다. 

 

  1. 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

Ethereum Smart Contract Best Practices - delegatecall

EIP-2535 Diamond Standard

OpenZeppelin ReentrancyGuard

+ Recent posts