12. Solidity(업그레이드)

정예찬·2022년 9월 11일
0

solidity

목록 보기
13/13

본 글은 freeCodeCamp.Org의 Youtube 영상 'Solidity, Blockchain, and Smart Contract Course – Beginner to Expert Python Tutorial'와 관련 코드인 SmartContract의 Github 코드를 기초로 작성되었다.

Youtube 영상 링크: https://www.youtube.com/watch?v=M576WGiDBdQ&t=10336s
Github 코드 링크: https://github.com/smartcontractkit/full-blockchain-solidity-course-py
오늘의 코드: https://github.com/PatrickAlphaC/upgrades-mix

이번 포스팅은 유튜브 영상 11:49:15~12:48:06에 해당하는 내용이다.

이번 시간에는 스마트 컨트랙트 업그레이드에 대해 다뤄보자. 영상에서는 총 3가지 방법을 이야기한다. 하나씩 살펴보자.

1. 매개변수화(Parameterize)

컨트랙트를 작성할 때 업그레이드가 진행될 대상을 사전에 변수로 설정하는 작업을 매개변수화라고 한다. 매개변수화는 변수의 값만 변경하는 작업이므로 업그레이드가 아니라고 보는 시각도 있다. 매개변수화의 문제는 사전에 매개변수로 설정하지 않은 변수를 업그레이드하고 싶어도 업그레이드가 불가능하다는 점이다. 또한 변수의 값을 누가 설정할 것인가도 고려해야 한다. 거버넌스로 설정하지 않으면 중앙화 시스템이 되기 때문이다.

2. 사회적 YEET(Social YEET) 또는 이주(Migration)

새로운 버전의 컨트랙트로 옮겨가는 업그레이드를 사회적 YEET 혹은 이주라고 한다. 이 경우 기존 컨트랙트 이용자들의 동의가 필요하다. 기존 컨트랙트를 변경하지 않기에 블록체인의 가치를 해치지 않으면서도 업그레이드가 이루어졌다는 검증을 누구나 할 수 있다는 장점을 지닌다. 반면 사용자들의 동의를 일구는데 많은 노력이 필요하고 업그레이드에 따른 주소 변경이 발생한다는 단점이 있다.

3. 프록시(Proxies)

프록시는 프로그램적인 업그레이드 중 가장 참된 형태라고 볼 수 있다.
프록시에서는 delegatecall을 이용한다. delegatecall이란, msg sender 및 msg value는 변하지 않으면서 타겟 주소의 계약이 실행되는 호출이다. 예를 들어 A 계약에서 B 계약을 호출하여 B 계약의 논리(logic)을 실행하는 B 계약에 대한 A 계약의 호출이 delegatecall이다.
프록시 계약의 주소는 영구히 보존된다. 업그레이드가 필요하면 새로운 실행 계약을 작성한 후 프록시 계약으로 실행 계약을 가리키기만 하면 되기 때문이다.

영상과 본 포스팅에서는 프록시를 메인으로 다룬다. 프록시에서 사용되는 용어를 몇 가지 살펴보자.
1. The implementation contract(실행 계약)
프로토콜의 모든 코드를 저장하는 계약이다. 업그레이드 때 새로운 실행 계약이 실행된다.

  1. The proxy contract(프록시 계약)
    실행 계약을 지정하는 계약이다. 함수 호출을 실행 계약으로 연결해준다.

  2. The user(사용자)
    프록시를 호출하는 자

  3. The admin(관리자)
    새로운 실행 계약으로 업그레이드하는 사용자(혹은 그룹, 투표자)

프록시 사용 시 발생할 수 있는 대표적 문제 2가지를 알아보자.

  1. Storage Clashes(저장소 충돌)
    예시를 들어 설명하겠다.
// Sample code, do not use in production!
contract Proxy {
    address implementation;
}
// Sample code, do not use in production!
contract Box {
    address public value;
    function setValue(address newValue) public {
        value = newValue;
    }
}

Proxy 계약과 Box라는 이름의 implementation 계약이 각각 하나 있다. proxy 계약을 실행하면 implementation이 저장소의 0번 공간에 저장된다. Box의 value 또한 0번 저장소에 할당되어 있다. delegatecall을 이용해 Box의 setValue 함수를 호출하여 value를 설정하면 value의 값은 Box 계약의 저장소가 아니라 Proxy 계약의 저장소에 저장된다. 따라서 implementation 위에 value가 덧씌워진다. 따라서 implementation 계약에서 다루는 값이 proxy에서 다루는 값과 같은 공간에 저장되지 않게 설정해주어야 한다. 아래 코드를 보자.

// Sample code, do not use in production!
contract Box {
    address implementation_notUsedHere;
    address public value;
    function setValue(address newValue) public {
        value = newValue;
    }
}

이 경우 implementation_notUsedHere라는, 우리가 설정하지도, 변경하지도 않는 값을 저장소 0번 공간에 할당함으로써 value를 1번 공간에 할당되게 했다. 이처럼 proxy 함수가 활용하는 저장소 공간을 implementation function에서 비워줌으로써 storage clash를 예방할 수 있다.

  1. Function Clashes(함수 충돌)

함수를 bytecode level에서 확인할 때 function selector라고 불리는 4-byte hash를 활용한다. 4바이트밖에 안 되기 때문에 다른 함수 간에 function selector가 겹칠 수 있다. 따라서 함수 호출 시 우리가 호출하고 싶은 function이 아니라 그 함수와 function selector가 같은 엉뚱한 함수가 호출될 수 있다. 이 같은 현상을 Function Clash라고 한다.
같은 계약 안에서 function selector가 같은 함수 2개가 발생하면 solidity compiler가 찾아준다. 그러나 proxy 계약 A와 implementation 계약 B의 function selector가 같다고 해도 solidity compiler는 이를 찾아내지 못한다. proxy는 delegatecall 이외에 upgradeTo(address)처럼 자체적인 함수를 갖는다. 따라서 proxy의 upgrade 관련 함수와 implementation의 함수 간 Function Clash가 발생할 수 있다.
이 문제를 해결하기 위해 Transparent proxy pattern이 이용된다. Transparent proxy는 계약 호출자의 주소에 기초하여 delegatecall의 사용을 결정한다. Transparent proxy는 계약 호출자가 admin(관리자)이면 delegatecall을 하지 못하게 한다. admin은 proxy의 자체적인 함수만 사용할 수 있게 된다. transparent proxy는 또한 admin을 제외한 나머지 user들은 반드시 delegatecall만 사용하게 한다. Function Clash는 이와 같은 방법으로 해결된다.

추가적으로 Proxy의 독특한 형태 2가지를 더 살펴보자.

  1. Universal Upgradable Proxies(UUPS)

Universal Upgradable Proxy는 누구에게나 업그레이드 권한을 주는 proxy이다. 이 경우 upgrade 관련 함수들은 implementation contract에 포함된다.

  1. Diamond Pattern

Diamond pattern은 다중 implementation을 가능하게 하는 proxy pattern이다. Diamond proxy에서는 implementation contracts가 나뉘어 있어서 분절적 upgrade가 가능하다.

본격적으로 smart contract를 활용한 upgrade를 진행해보자!

demos 폴더에 upgrades 폴더를 만들고 brownie로 initialize해주자.

늘 그래왔듯 .env를 아래 코드로 채워주자.

export PRIVATE_KEY = 0xb5e857091a491a306f8a13ac49ed53655f51c2d780db0e9faf0305878b3f8fe5
export WEB3_INFURA_PROJECT_ID=a9c5bc0ec75c4a83a3e48086df81acfe
export ETHERSCAN_TOKEN=BEF1HMKFS34JWYP6RQACG8S3MX6G4FN4N4

다음으로 brownie-config.yaml을 아래 코드로 채워주자.

reports:
  exclude_contracts:
    - SafeMath
dependencies:
  - OpenZeppelin/openzeppelin-contracts@4.1.0
compiler:
  solc:
    remappings:
      - '@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.1.0'
# automatically fetch contract sources from Etherscan
autofetch_sources: True
dotenv: .env
networks:
  default: development
  development:
    verify: False
  rinkeby:
    verify: False
  ganache:
    verify: False
wallets:
  from_key: ${PRIVATE_KEY}
  from_mnemonic: ${MNEMONIC}

autofetch_sources는 필수는 아니지만 default로 넣어주는 코드이다. Etherscan과 같은 block explorer가 활용된 source code를 자동으로 패치해주는 코드이다.
from_mnemonic: ${MNEMONIC}도 from_key: ${PRIVATE_KEY}와 같은 맥락에서 활용된다. 이 경우 ${MNEMONIC}는 MNEMONIC을 환경변수화, 즉 암호화한 상태이다.
openzeppelin을 활용
나머지는 기존에 살펴본 내용이니 설명은 생략한다.

먼저 업그레이드를 진행하기 위해 contracts 폴더를 채워주자. 이번 시간에는 transparent proxy를 활용한다. transparent proxy 활용을 위한 solidity 파일 2개를 github의 openzeppelin-contracts에서 가져오자. contracts 폴더에 transparent_proxy 폴더를 만들고 TransparentUpgradeableProxy.sol과 ProxyAdmin.sol을 추가해주자. 그리고 아래 링크에 서 코드를 복사하여 내용을 추가한 파일의 내용을 채워주자.
https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy/transparent
이때 import가 정상적으로 이루어질 수 있도록 두 파일의 import문만 아래와 같이 수정해주자.

TransparentUpgradeableProxy.sol의 import문:

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

ProxyAdmin.sol의 import문:

import "./TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

upgrade를 위한 implementation contracts 2개를 작성해주자. Box.sol에서 BoxV2.sol로의 업그레이드를 진행할 텐데, Box.sol과 BoxV2.sol 모두 contracts 폴더에 작성해주면 된다. Box.sol부터 작성해보자.

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
contract Box {
    uint256 private value;
 
    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);
 
    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }
 
    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
}

Box contract는 값을 보관하고 불러오는 계약이다.
값을 저장하기 위해 정수 value를 선언하였다. 이때 계약 내부에서만 값이 변경되도록 private 키워드를 추가하였다.
다음으로 value가 변화하였을 때의 log 추적을 위해 ValueChanged라는 event를 추가하고 있다. value를 저장(변화)하는 함수 store를 선언하고 있다. store는 값을 하나 받아 value에 저장하고 ValueChanged를 emit한다.
retrieve 함수는 호출되면 value를 반환한다.

이번에는 BoxV2.sol을 작성하자. BoxV2 contract는 Box에 increment라는 함수 하나를 추가한 contract이다.

// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
contract BoxV2 {
    uint256 private value;
 
    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);
 
    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }
    
    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
    
    // Increments the stored value by 1
    function increment() public {
        value = value + 1;
        emit ValueChanged(value);
    }
}

increment function을 제외하고는 Box contract와 내용이 동일하니 increment function만 보자. increment는 value에 1을 더하여 저장하고, ValueChanged를 emit한다.

본격적으로 scripts를 작성하기 전에 __init__.py를 scripts 폴더에 추가해주자.

scripts 작성을 시작하자. scripts 폴더에 helpful_scripts 파일을 추가해주자. 코드가 조금 긴 편이므로 두 번에 나누어 설명하겠다.

from brownie import network, accounts, config
import eth_utils

NON_FORKED_LOCAL_BLOCKCHAIN_ENVIRONMENTS = ["hardhat", "development", "ganache"]
LOCAL_BLOCKCHAIN_ENVIRONMENTS = NON_FORKED_LOCAL_BLOCKCHAIN_ENVIRONMENTS + [
    "mainnet-fork",
    "binance-fork",
    "matic-fork",
]


def get_account(number=None):
    if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS:
        return accounts[0]
    if number:
        return accounts[number]
    if network.show_active() in config["networks"]:
        account = accounts.add(config["wallets"]["from_key"])
        return account
    return None


def encode_function_data(initializer=None, *args):
    """Encodes the function call so we can work with an initializer.
    Args:
        initializer ([brownie.network.contract.ContractTx], optional):
        The initializer function we want to call. Example: `box.store`.
        Defaults to None.
        args (Any, optional):
        The arguments to pass to the initializer function
    Returns:
        [bytes]: Return the encoded bytes.
    """
    if not len(args): args = b''

    if initializer: return initializer.encode_input(*args)

    return b''

eth_utils를 import하고 있지만 정작 script에서는 사용하지 않는다. 이는 영상 촬영 이후 upgrades-mix의 코드가 변경된 탓으로 보인다. 영상에서는

if not len(args): args = b''

if initializer: return initializer.encode_input(*args)

return b''

위 코드 대신 아래 코드를 사용한다.

if len(args) == 0 or not initializer:
	return eth_utils.to_bytes(hexstr="0x")
    
return initializer.encode_input(*args)

두 코드 모두 같은 내용이나 표현 방식에 차이가 있을 뿐이다. eth_utils를 사용하던 코드에서 사용하지 않는 코드로 변경되어 변경 전의 잔재가 남아있는 듯하다.
eth_utils 사용을 위해서는 터미널에 'pip install eth_utils'를 입력해주어야 한다.

blockchain environments 설정에도 조금 차이가 생겼는데 Local blockchain environments를 forked environments와 non-forked environments로 나누어 각각에 필요한 environments를 추가해주고 있다.

get_account 함수는 생략하고 encode_function_data 함수를 보자. 이 함수를 정의하는 이유를 알기 위해서는 initializer를 이해할 필요가 있다. proxy에는 constructor가 없다. proxy는 constructor 대신 initializer를 사용한다. 그러나 initializer는 bytecode로 부호화(encode)하여 전달되어야 한다. 이를 위한 함수 정의가 encode_function_data이다.
encode_function_data는 입력값으로 initializer와 *args를 받고 있다. *args는 여러 개의 input을 대체한다. 즉, n개의 input을 넣어야 하는데 n이 정해지지 않았을 때 *args를 사용하면 input을 원하는 수만큼 넣을 수 있다.(이때 *뒤에 붙는 값은 꼭 args일 필요는 없다. arguments가 되어도 되고, a가 되어도 되고, wereasfwe가 되어도 되고, 원하는 단어 아무거나 가능하다.) 즉, encode_function_data의 input은 'initializer, 2, 4, 5', 'initializer, 3, 2, 5, 6, 7' 등 다양한 개수가 될 수 있다. 코드에 달린 각주를 읽어보면 이해가 더 잘 될 것이다.
len(args)가 0이면 if not 구문이 실행된다. args는 입력된 input의 길이를 반환하므로 len(args)가 0이라는 말은 입력된 args가 없다는 말이다. 이때는 NULL을 bytecode화 하여 args에 저장한다. b'string'은 string을 bytecode로 만들어 반환한다.
initializer.encode_input(*args)은 initializer 함수를 input과 함께 bytecode로 변경하여준다. 따라서 initializer가 전달된 경우 initializer.encode_input(*args)를 실행하여 반환한다.
if initializer가 참이 아닌 경우, 즉 initializer가 전달되지 않은 경우에는 NULL을 bytecode화하여 반환한다.

def upgrade(
    account,
    proxy,
    newimplementation_address,
    proxy_admin_contract=None,
    initializer=None,
    *args
):
    transaction = None
    if proxy_admin_contract:
        if initializer:
            encoded_function_call = encode_function_data(initializer, *args)
            transaction = proxy_admin_contract.upgradeAndCall(
                proxy.address,
                newimplementation_address,
                encoded_function_call,
                {"from": account},
            )
        else:
            transaction = proxy_admin_contract.upgrade(
                proxy.address, newimplementation_address, {"from": account}
            )
    else:
        if initializer:
            encoded_function_call = encode_function_data(initializer, *args)
            transaction = proxy.upgradeToAndCall(
                newimplementation_address, encoded_function_call, {"from": account}
            )
        else:
            transaction = proxy.upgradeTo(newimplementation_address, {"from": account})
    return transaction

다음으로 upgrade 함수를 보자. 이 함수가 helpful_scripts.py의 마지막 내용이다.
upgrade를 정의하고 있는데, upgrade를 호출하는 계좌인 account, upgrade 대상 proxy, 새로운 implementation contract, proxy_admin의 contract, initializer와 *args를 입력값으로 받는다.
upgrade는 크게 2가지로 나눌 수 있다. proxy_admin_contract가 존재하는 경우와 그렇지 않은 경우이다. 두 경우를 나누어 살펴보자.
1. proxy_admin_contract가 존재하는 경우
i)initializer가 전달된 경우
먼저 encode_function_data를 호출하여 initializer를 encode한다.
proxy_admin_contract에 대해 upgradeAndCall 함수를 호출하여 proxy 주소, 새로운 implementation 계약 주소, encode된 initializer 정보를 input으로 transaction을 발생시킨다.
ii) initializer가 없는 경우
initializer가 없으면 함수를 upgrade까지만 하고 call은 하지 않아도 된다. 따라서 proxy_admin_contract에 대해 upgrade 함수를 호출하여 newimplementation_address(새로운 implementation 계약 주소)와 encoded_function_call(encode된 initializer 정보)를 입력값으로 넣어준다.
2. proxy_admin이 호출자가 아닌 경우
이때는 proxy_admin 지정 없이 upgrade를 진행할 proxy에 대해 implementation 계약을 바로 지정해주면 되므로 upgradeToAndCall 함수 또는 upgradeTo 함수를 사용한다.
i) initializer가 전달된 경우
encode_function_data로 initializer를 encode한다.
proxy에 대해 newimplementation_address, encoded_function_call을 인자로 upgradeToAndCall을 호출한다.
ii) initializer가 없는 경우
proxy에 대해 upgradeTo를 호출한다. 이때 newimplementation_address가 인자로 전달된다.

이번에는 box를 deploy하고 1을 store하는 script인 01_deploy_box.py를 작성해보자. 영상과는 다르게 box를 deploy하는 script와 upgrade를 진행하는 script를 나누어 설명하겠다. 필자가 설명을 위해 script를 일부 수정한 부분도 있으니 원본 script를 참고 바란다.

from brownie import (
    Box,
    TransparentUpgradeableProxy,
    ProxyAdmin,
    config,
    network,
    Contract,
)
from scripts.helpful_scripts import get_account, encode_function_data


def main():
    account = get_account()
    print(f"Deploying to {network.show_active()}")
    box = Box.deploy(
        {"from": account},
        publish_source=config["networks"][network.show_active()]["verify"],
    )
    proxy_admin = ProxyAdmin.deploy(
        {"from": account},
    )
    
    box_encoded_initializer_function = encode_function_data()
    proxy = TransparentUpgradeableProxy.deploy(
        box.address,
        proxy_admin.address,
        box_encoded_initializer_function,
        {"from": account, "gas_limit": 1000000},
    )
    print(f"Proxy deployed to {proxy} ! You can now upgrade it to BoxV2!")
    proxy_box = Contract.from_abi("Box", proxy.address, Box.abi)
    print(f"Here is the initial value in the Box: {proxy_box.retrieve()}")
    proxy_box.store(1, {"from": account})
    print(f"Now the value in the Box is: {proxy_box.retrieve()}")

메인 함수를 보자. box와 proxy_admin을 deploy하고 있다.
위 script에서는 별도의 initializer 설정을 하지 않았다. 그러나 원하는 경우 encode_function_data에 input을 입력하여 설정이 가능하다.
box를 implementation contract로 proxy를 deploy하고 있다. 이때 input은 box의 주소, proxy_admin의 주소, box_encoded_initializer_function가 된다.
기존에 우리가 deploy된 contract의 function 호출을 할 때에는 contract.function(input) 형태로 호출을 진행했다. 예컨대 Box의 store를 이용하여 value에 1을 저장하고 싶으면 Box.store(1)를 실행했다. 그러나 이 경우 proxy를 이용하지 않기에 Box를 BoxV2로 upgrade하면 기존 value의 값은 초기화된다. 따라서 upgrade 이후에도 upgrade 이전 implementation contract의 실행 결과가 보존되게 하기 위해서는 기존에 사용했던 방법과는 다른 방법을 사용해야 한다.
ABI로부터 proxy contract를 instance화하여 저장해주어야 한다. 이는 Contract.from_abi("인스턴스 명칭", 인스턴스화할 대상의 주소, ABI) 형태로 구현된다.
따라서 Contract.from_abi("Box", proxy.address, Box.abi)를 proxy_box에 저장한 이후 proxy_box를 대상으로 함수를 호출하면 Box를 implementation contract로 하는 proxy를 대상으로 하는 함수 호출이 된다.
proxy_box.store(1, {"from": account})를 진행한 후 upgrade를 진행하더라도 value에 저장된 1 값은 그대로 남아있게 된다. 이는 다음 script를 통해 확인하자. 02_upgrade_box.py를 scripts 폴더에 추가한 후 다음 내용을 추가하자.

from brownie import (
    BoxV2,
    TransparentUpgradeableProxy,
    ProxyAdmin,
    config,
    network,
    Contract,
)
from scripts.helpful_scripts import get_account, upgrade


def main():
    account = get_account()
    print(f"Deploying to {network.show_active()}")
    box_v2 = BoxV2.deploy(
        {"from": account},
        publish_source=config["networks"][network.show_active()]["verify"],
    )
    proxy = TransparentUpgradeableProxy[-1]
    proxy_admin = ProxyAdmin[-1]
    upgrade(account, proxy, box_v2, proxy_admin_contract=proxy_admin)
    print("Proxy has been upgraded!")
    proxy_box = Contract.from_abi("BoxV2", proxy.address, BoxV2.abi)
    print(f"Starting value {proxy_box.retrieve()}")
    proxy_box.increment({"from": account})
    print(f"Ending value {proxy_box.retrieve()}")

main 함수를 보자. BoXV2를 deploy하여 box_v2에 저장한다.
최근 deploy된 TransparentUpgradeableProxy와 ProxyAdmin를 각각 proxy와 proxy_admin에 저장한다.
account, proxy, box_v2, proxy_admin을 입력값으로 upgrade를 실행한다. upgrade에 관한 설명은 helpful_scripts.py에서 충분히 진행하였다.
01_deploy_box.py에서와 비슷한 맥락으로 BoxV2의 abi로부터 proxy를 instance화하고, 이를 proxy_box에 저장한다.
BoxV2의 increment함수를 proxy_box를 대상으로 실행하여 value를 1 증가시키고, retrieve 함수를 호출하여 value에 저장된 값을 확인한다.

01_deploy_box.py를 실행하여 value에 1을 저장한 후 02_upgrade_box.py를 실행하면 increment 함수에 의해 value가 2가 됨을 확인할 수 있다.

마지막으로 test 2가지를 진행하자. 먼저 test_box_proxy.py를 tests 폴더에 추가하고 아래 내용을 입력해주자. test_box_proxy.py는 proxy_box가 올바르게 동작하는지 확인하는 테스트이다.

import pytest
from brownie import (
    Box,
    TransparentUpgradeableProxy,
    ProxyAdmin,
    Contract,
    network,
    config,
)
from scripts.helpful_scripts import get_account, encode_function_data


def test_proxy_delegates_calls():
    account = get_account()
    box = Box.deploy(
        {"from": account},
        publish_source=config["networks"][network.show_active()]["verify"],
    )
    proxy_admin = ProxyAdmin.deploy(
        {"from": account},
    )
    box_encoded_initializer_function = encode_function_data()
    proxy = TransparentUpgradeableProxy.deploy(
        box.address,
        proxy_admin.address,
        box_encoded_initializer_function,
        {"from": account, "gas_limit": 1000000},
    )
    proxy_box = Contract.from_abi("Box", proxy.address, Box.abi)
    assert proxy_box.retrieve() == 0
    proxy_box.store(1, {"from": account})
    assert proxy_box.retrieve() == 1
    with pytest.raises(AttributeError):
        proxy_box.increment({"from": account})

test_proxy_delegates_calls 함수를 보자. proxy_box 설정까지는 01_deploy_box.py와 동일하니 설명은 생략하겠다. 3가지 assertion만 보자.
proxy_box를 설정한 후 store 함수를 아직 실행하지 않은 상태에서는 value가 0이다. 이 value를 retrieve할 수 있는지 assert한다.
다음으로 store 함수를 이용하여 value에 1을 저장한 후 1이 제대로 저장되었는지 retrieve 함수를 이용하여 확인한다.
마지막으로 BoxV2에서만 작동하는 increment가 Box에서 작동하지는 않는지 확인한다.

이번에는 tests 폴더에 test_box_v2_upgrades.py를 추가하고 아래 내용으로 채워주자. test_box_v2_upgrades.py는 Box에서 BoxV2로의 upgrade가 똑바로 진행되는지 확인한다.

import pytest
from brownie import (
    Box,
    BoxV2,
    TransparentUpgradeableProxy,
    ProxyAdmin,
    Contract,
    network,
    config,
    exceptions,
)
from scripts.helpful_scripts import get_account, encode_function_data, upgrade


def test_proxy_upgrades():
    account = get_account()
    box = Box.deploy(
        {"from": account},
    )
    proxy_admin = ProxyAdmin.deploy(
        {"from": account},
    )
    box_encoded_initializer_function = encode_function_data()
    proxy = TransparentUpgradeableProxy.deploy(
        box.address,
        proxy_admin.address,
        box_encoded_initializer_function,
        {"from": account, "gas_limit": 1000000},
    )
    box_v2 = BoxV2.deploy(
        {"from": account},
    )
    proxy_box = Contract.from_abi("BoxV2", proxy.address, BoxV2.abi)
    with pytest.raises(exceptions.VirtualMachineError):
        proxy_box.increment({"from": account})
    upgrade(account, proxy, box_v2, proxy_admin_contract=proxy_admin)
    assert proxy_box.retrieve() == 0
    proxy_box.increment({"from": account})
    assert proxy_box.retrieve() == 1

proxy에 Box를 소재로 deploy한 후 proxy에 BoxV2.abi를 입혀 proxy_box에 저장한다.
이때 proxy_box는 BoxV2의 함수를 구현하지 못한다.
proxy_box의 increment를 실행함으로써 에러 발생을 확인하고 있다.
upgrade를 통해 proxy의 소재를 BoxV2로 바꿔준 이후에는 proxy_box가 올바르게 동작해야 한다. retrieve 함수가 초기값인 0을 제대로 저장하고 있는지 확인한 후 increment를 실행하고 retreive를 실행하여 upgrade의 효과를 확인한다.

upgrade와 관련한 포스팅도 마쳤다! 원래 한참 전에 올리려고 했는데 전역하고 정신이 없었다. 정말 바빴지만 의미가 넘치는 일들로 채워나갔다. 이제 포스팅은 에필로그 제외하고 1편 남았다. Full Stack Defi 관련한 마지막 포스팅은 분량이 3시간이 넘는다. 포스팅보다 우선순위로 해야 하는 일들이 훨씬 많아서 마지막 포스팅은 일단 미뤄두겠다. 여기까지 읽어준 독자가 있다면 정말 고맙고 마지막 포스팅에서 다시 보자!

MIT License

Copyright (c) 2021 SmartContract

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
profile
BlockChain Researcher

0개의 댓글