Proxy Contract

_Block·2022년 5월 1일
0

SmartContract

목록 보기
1/5
post-thumbnail

업무를 진행하면서 공부하고 작성 하였던 Proxt Contract에 대한 기록 입니다.

🐾 Proxy Contract

블록체인이 매력적인 이유는 제 개인적으로는 immutability라는 특징을 가지고 있기 때문에 매력적이라고 생각을 합니다.

수정이 불가능하고, 그러기 떄문에 개발자들은 많은 부분을 고려해야하며, 만약 잘못된 코드가 작성이 되면 해당 프로젝트는 순식간에 실패한 프로젝트가 되겠지만

이러한 특징으로 인해 사람들이 서로 신뢰할수 있기, 특정 프로젝트의 Owner가 불합리한 행위를 할 수 없는 특징이 있습니다.

일반적으로 양날의 칼 일수도 있습니다.

하지만 Bug가 없는 시스템은 없고, 프로젝트의 Owner가 로직에서는 불합리성이 없지만 이후 추가적인 기능을 활용하고자 할떄에는 컨트래트를 수정하는 것이 굉장히 효율적으로 동작하게 될 것입니다.

만약 A라는 프로젝트가 합리적으로 잘 돌아가고 있는데 이러한 A라는 프로젝트에 또 다른 기능을 넣고자 한다면 Proxy Contract는 굉장히 효율적인 방법이 될 것입니다.

🐾 동작 원리

가장 기본적인 구조의 Proxy Contract입니다.

  • Proxy Contract에도 다양한 디자인 패턴이 있습니다.

🐾 전체 코드

Proxy Contract가 주로 활용되는 방법은 기존 컨틀랙트에 기능을 추가할떄에 사용이 됩니다.

또한 큰 특징으로는 기존 컨트랙트의 데이터를 담고 있다는 특징이 있습니다.

pragma solidity ^0.4.21;



contract UpgradeableContractProxy  {

    address private _contractAddress;

    function updateImplementation(address _newImplementation)  public {
        require(_newImplementation != address(0));

        _contractAddress = _newImplementation;
    }

    function implementation() public view returns (address) {
        return _contractAddress;
    }

    function () payable public {
        address _impl = implementation();
        require(_impl != address(0));

        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize)
            
            let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
            
            let size := returndatasize
            returndatacopy(ptr, 0, size)

            switch result
            case 0 { revert(ptr, size) }
            case 1 { return(ptr, size) }
        }
    }
}

일단 기본적으로 이전에는 사용해 보지 않았던 방법으로 코드가 동작을 하게 됩니다.

  • 대표적으로 fallback(), assembly

그러기 때문에 이 두가지를 먼저 점검하고 코드에 대한 설명을 하도록 하겠습니다.

🐾 fallback

만약 SDK를 활용하여 Contract를 활용한다는 가정하에 Contract에 없는 함수를 실행시키면 어떻게 처리할지에 대한 구문을 적게 됩니다.

일반적으로 간단하게 이벤트를 출력하지만 저희는 이러한 방법을 통해서 Proxy를 구현 할수 있습니다.

  • fallback에 대해서 좀더 상세한 설명은 https://dayone.tistory.com/36 이쪽을 참고하면 좋을듯 싶습니다.

🚀 여기에서는 간단하게 존재하지 않는 메서드를 호출할때 발생하는 함수 정도로 이해하면 됩니다.

🐾 assembly

이 부분에 대해서는 굉장히 난해한 부분 입니다.

일단 저도 assembly에 있는 모든 함수들을 이해하지 못하고, 자유롭게 활용이 가능한 수준은 아닙니다.

간단하게 말하면

assembly를 통해서 Low-Level언어로 접근이 가능하고, 메모리나 스토리지에 직접적으로 접근이 가능한 방법을 말합니다.

  • 이외에도 가스비가 덜 소요 된다는 장점이 있습니다.

이러한 방법이 왜 활용이 가능하냐면

EVM에서는 자체적으로 VM과 같이 동작을 하게 됩니다.

그러기 떄문에 들어오는 코드, 함수 들은 바이트 코드로 변환이 되어 스택에 담기게 되고 이후 처리를 하게 됩니다.

  • 이 들어오는 데이터에 접근이 가능한 방법이 assembly입니다.

바이트 코드로 변환이 되는 이유는 모두 다른 개발환경에서 같은 결과값을 도출해 내기 위함입니다.

  • https://steemit.com/kr/@kanghamin/evm-solidity-what-is-evm-and-solidity 이 부분을 참고하면 좋을 것 같습니다.

🐾 셋팅 방법

저희가 web3js SDK를 활용한다고 하였을때 컨트랙트 활용은 이와 같이 합니다.

const web3 = new Web3.eth.Contract(abi, CA)

이 부분에서 일단 A라는 컨트랙트를 프록시 컨트랙트로 잡고자 한다면

A, Proxy Contract를 모두 배포를 해줍니다.

  • A의 CA 주소 : A_CA
  • Proxy Contract의 CA 주소 : Pro_CA

이런 상황이라면 저희가 Contract를 활용할 때에는

const web3 = new Web3.eth.Contract(A_abi, Pro_CA)

처럼 활용을 하게 됩니다.

  • 이 부분은 Proxy Contract에서 updateImplementation라는 함수를 통해 A의 CA주소를 할당해야 동작을 하게 됩니다.
👆 정리하자면

1. A, Proxy 배포

2. Proxy의 updateImplementation함수에 A의 CA주소 할당

3. 이후 SDK를 활용

그후 Client에서 트랜잭션을 전송할 때에는 이와 같이 전송을 합니다.

const tx = {
	from : 'my wallet address',
    to : "Pro_CA",
    gas : 5000000,
    data : Contract.methods.함수명().encodeABI
}

자 여기에서 코드를 보면 알수 있듯이 To의 주소 값이 Proxy Contract입니다.

문제는 SDK를 통해서 불러온 Contract에는 A의 함수들이 담겨 있습니다.

  • 이 부분은 실제로 불러오시면 알 수 있습니다.

Proxy Contract에 트랜잭션을 전송하지만 Proxy Contract에 없는 함수를 실행시키는 트랜잭션을 전송하는 것 입니다.

🐾 코드 분석

그러면 저희는 얼추 assembly, fallback이 무엇인지를 알게 되었으니 코드가 동작하는 원리를 살펴보겠습니다.

가장 중요한 부분은 fallback부분 입니다.

function () payable public {
        address _impl = implementation();
        require(_impl != address(0));

        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize)
            
            let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
            
            let size := returndatasize
            returndatacopy(ptr, 0, size)

            switch result
            case 0 { revert(ptr, size) }
            case 1 { return(ptr, size) }
        }
    }

앞서 적은것과 같이 Contract에 없는 함수를 호출하기 떄문에 fallback함수가 동작을 하게 될 것입니다.

이때 저희는 assembly를 통해서 msg.data에 접근을 하게 됩니다.

👆
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
👆

이 부분은 메모리 할당 부분에 대한 코드 입니다.

Solidity에 빈 메모리 공간을 mload를 통해서 ptr이라는 변수에 할당해 줍니다.

  • 0x40메모리 주소는 항상 남아있는 free memory 공간을 가르키게 됩니다.

그후 datacopy를 통해서 ptr변수에 msg.data의 size값을 할당해 줍니다.

  • data를 할당해 주는 것이 아니라 크기를 할당해 줍니다.

👆
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
👆

이후 실질적으로 다른 컨트랙트를 호출하는 delegatecall을 호출해 줍니다.

gas : 말그대로 사용되는 gas양 입니다.
_impl : 코드를 보면 알수 있듯이 A 컨트랙트를 가르키게 됩니다.
ptr : 보낼 데이터의 메모리 주소 입니다.
calldatasize : 데이터의 크기 입니다.
0 : 해당 값의 return값 입니다.

  • 어떤 값이 return될지 모르기 때문에 0으로 처리 합니다.

이런식으로 동작을 하게 되면

_impl = Contract의 주소ptr = data이 주소가 요청이 되게 될 것입니다.

  • 즉 SDK를 통해서 호출한 트랜잭션이 실질적인 주소값으로 트랜잭션이 전송이 되게 됩니다.

👆
let size := returndatasize returndatacopy(ptr, 0, size)
switch result case 0 { revert(ptr, size) } case 1 { return(ptr, size) }
👆

이후 Return데이터를 받고 해당 데이터를 검증함으로써 마무리가 됩니다.

이후 만약 A의 컨트랙트를 수정 한다면 A의 컨트랙트를 상속하는 B컨트랙트의 주소를 다시 Proxy컨트랙트에 지정해주면 됩니다.

profile
Block_Chain 개발자 입니다. 해당 블로그는 네트워크에 관한 내용을 다루고 있습니다.

0개의 댓글