[Solidity] Proxy contract part 3 - Proxy Admin

임형석·2023년 11월 4일
0

Solidity


Proxy Admin

part 2 에서는 유저와 어드민의 인터페이스를 나누었고 이번에는 프록시의 관리를

위해 ProxyAdmin 이라는 컨트랙트를 만들어 관리를 한다.

왜 나누어야 하는지?

아래와 같이 프록시, 로직 컨트랙트에는 같은 이름을 가진 함수가 있다.

- Proxy
    function admin() external checkAdmin returns(address) {
        return _getAdmin();
    }

    function implementation() external checkAdmin returns(address)  {
        return _getImplementation();
    }
    
- CounterV1
    function admin() external pure returns(address) {
        return address(1);
    }

    function implementation() external pure returns(address) {
        return address(2);
    }

그리고 이 함수는 아래의 checkAdmin modifier 를 통해 유저와 어드민 인터페이스로 나누어진다.

    modifier checkAdmin() {
        if(msg.sender == _getAdmin()){
            _;
        } else {
            _fallback();
        }
    }

어드민 이라면 Proxy 컨트랙트 내부의 함수를 호출,
유저라면 CounterV1 인 로직 컨트랙트의 함수를 호출.

따라서, 어드민이 로직 컨트랙트의 함수를 호출할 수 있는 방법이 없다.

그래서 ProxyAdmin 컨트랙트를 생성하여admin(나) => ProxyAdmin => Proxy => CounterV1~ 의 순서대로 컨트랙트를 제어하고 어드민이 로직 컨트랙트 함수를 호출 할 수 있도록 코드를 수정한다.


Proxy 컨트랙트 수정

먼저 Proxy 컨트랙트에 함수를 추가한다. 현재 Proxy 컨트랙트 내에는 admin 을 수정하는 함수가 있지만, 생성자에 의해 컨트랙트가 만들어질때에만 사용된다.

따라서, 새로 만드는 ProxyAdmin 컨트랙트로 admin 을 변경해주어야 하기에 아래의 함수를 추가한다.

    function changeAdmin(address _admin) external checkAdmin {
        _setAdmin(_admin);
    }

ProxyAdmin 컨트랙트

아래와 같이 필수적인 코드를 적어준다. admin 으로 설정할 주소를 상태변수로 선언하고 생성자로 배포한 주소를 admin 으로 넣어준다.

그리고 onlyOwner modifier 를 만들어준다.

contract ProxyAdmin {
    address public admin;

    constructor() {
        admin = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == admin, "Admin only.");
        _;
    }
}

그리고 현재 ProxyAdmin 컨트랙트의 admin 을 변경하는 함수도 만들고, 로직 컨트랙트을 업그레이드 하는 함수도 만들어준다.

    function changeProxyAdmin(address payable _Proxy, address _admin) external onlyOwner {
        Proxy(_Proxy).changeAdmin(_admin);
    }

    function upgrade(address payable _Proxy, address _implementation) external onlyOwner {
        Proxy(_Proxy).upgradeTo(_implementation);
    }

또한, 현재 Proxy 컨트랙트의 어드민 주소와 로직 컨트랙트 주소를 반환받는 함수를 구현한다.

    function getProxyAdmin(address _Proxy) external view returns(address) {
        (bool success, bytes memory res) = _Proxy.staticcall(abi.encodeCall(Proxy.admin, ()));
        require(success, "Failed to call.");
        return abi.decode(res, (address));
    }

    function getProxyImplementation(address _Proxy) external view returns(address) {
        (bool success, bytes memory res) = _Proxy.staticcall(abi.encodeCall(Proxy.implementation, ()));
        require(success, "Failed to call.");
        return abi.decode(res, (address));
    }

이때 사용되는 것은 staticcall 인데, 이것은 상태변수를 변경시키지 않는 call 이다. 왜 이것을 사용하는지 Proxy 컨트랙트의 함수를 확인해보자.

- Proxy
    function admin() external checkAdmin returns(address) {
        return _getAdmin();
    }

    function implementation() external checkAdmin returns(address)  {
        return _getImplementation();
    }
    
    modifier checkAdmin() {
        if(msg.sender == _getAdmin()){
            _;
        } else {
            _fallback();
        }
    }

어떠한 상태변수도 수정하지 않기 때문에 두 함수는 사실상 view 함수로 볼 수 있다.

하지만, 두 함수에 사용하는 checkAdmin modifier 내의 else 문에서 fallback 함수를 호출할 수도 있기에 view 설정이 불가능하다.

따라서, view 함수가 없으므로 상태변수를 수정하지 않는 staticcall 로 호출하는 것이다. 이렇게 해야 불필요한 가스의 소모 없이 함수를 호출할 수 있다.


배포 및 확인

위에서 설명한대로 어드민은 위와 ProxyAdmin 컨트랙트를 통해 전체 컨트랙트를 제어하게 되며, 어드민도 유저와 같이 로직 컨트랙트 함수를 호출할 수 있다. 직접 배포하고 확인해보았다.


컨트랙트 4개를 모두 배포한다.


Proxy 컨트랙트의 admin 을 ProxyAdmin 주소로 바꾼다.


ProxyAdmin 컨트랙트에서 CounterV1, Proxy 컨트랙트 주소를 인자로 넣고 업그레이드 해준다.


배포할 컨트랙트로 CounterV1 을 설정한 후, At Address 에 Proxy 컨트랙트 주소를 넣고 클릭.


함수를 호출하는 주소가 admin 임에도 유저와 같은 결과 값을 반환한다. 성공.

increase 함수로 count 상태변수를 5로 놓고 CounterV2 로 업그레이드.


CounterV2 로 업그레이드 했고, 상태변수 count 는 5 인 상태로 업그레이드까지 끝.


정리

오픈제플린의 프록시 라이브러리의 컨트랙트 코드를 뒤져보았지만, 사실 이해가 잘 되질 않았다.

하지만 직접 컨트랙트를 만들고 배포해보니 이런 방법으로 전체 컨트랙트가 관리되는구나.. 하고 알게되었다.

특히 잘 사용하지 않는 어셈블리, staticcall, abi.encodeCall, abi.decode 등을 사용해서 컨트랙트를 작성한게 좋은 경험이 된 것 같다.

작성한 코드는 여기에.. Proxy contract 실습 코드


0개의 댓글