1
0

Berachainでアップグレード可能なERC20トークンをデプロイする [Berachain翻訳]

Posted at

本記事は下記の翻訳となります。
『Deploy an Upgradeable ERC20 Token on Berachain』

image.png

アップグレーダブルなコントラクトの紹介

Berachain のようなブロックチェーン上のスマートコントラクトは、一度デプロイされると通常は不変です。これは確実性を提供しますが、バグの修正、機能の追加、そして急速に変化する環境に適応するためにコントラクトを更新する必要がある開発者にとっては課題となる可能性があります。アップグレーダブルなコントラクトは、不変性と柔軟性の間の解決策を提供します。

Proof-of-Liquidity とアップグレーダブル性

Berachain の新しい Proof-of-Liquidity(PoL)コンセンサスメカニズムに参加するプロトコルは、通常、ユーザーに Berachain のネイティブトークン$BGTの報酬を獲得するために、そのプロトコルへの預金を表すERC20 トークンをステーキングすることを要求します。
https://docs.berachain.com/learn/what-is-proof-of-liquidity?ref=berachain.ghost.io

プロトコルは最初にステーキングモデルから始まるかもしれませんが、プロトコルの機能を変更せずに報酬に少し創造性を加えたい場合はどうでしょうか?ここでは、アップグレーダブルなコントラクトがどのようにしてこれを可能にするかを探ります。

💡 トークンの移行なしに PoL ステーキングメカニズムを変更できます

アップグレーダブルコントラクトガイド - 概要

このガイドでは、FoundryOpenZeppelin のアップグレーダブルコントラクトを使用して、Berachain 上で ERC20 トークンを作成、デプロイ、アップグレードするプロセスを説明します。具体的には、以下のことを達成します:

  1. ERC20 コントラクト(v1 Implementation)をデプロイする
  2. v1 Implementationのロジックを継承する Proxy コントラクトをデプロイする
  3. PoL 報酬の時間ベースのブーストを実装するための新しい関数を持つ、修正された ERC20 コントラクト(v2 Implementation)をデプロイする
  4. Proxy をv2 Implementationのロジックを使用するようにアップグレードする

アップグレーダブルスマートコントラクトの仕組み

「Proxy」や「実装」という言葉は耳慣れないかもしれません。そこで、コードに入る前にいくつかの用語と概念を明確にしておきましょう。

  • **Proxy**は、ユーザーが直接やり取りするコントラクトです。コントラクトのデータと状態を保存する役割を担います。しかし、これは単なる外殻であり、機能やロジックは含まれていません。それは以下の役割です...

**Implementation**コントラクト。Implementationは、ユーザー向けのProxyコントラクトのためのすべてのコントラクトロジックをホストしますが、コントラクトアドレスにデータを保存することはありません。

上の図で示されているように、ProxyImplementationコントラクトは連携して動作します:

  • ユーザーはまずProxycallを行います。
  • リクエストはdelegatecallを使用して関連するImplementationコントラクトにルーティングされます。
  • Proxyコントラクトの許可された所有者は、異なるImplementationコントラクト間で切り替えることができます - つまり、アップグレード可能です!

アップグレーダブルコントラクトには様々な種類があります。このチュートリアルで使用されるタイプはUUPS(Universal Upgradeable Proxy Standard)で、アップグレードロジックをImplementationコントラクト自体に組み込んでいます。この設計はコントラクトの構造を簡素化し、Proxyのアップグレードを管理するための追加の管理コントラクトの必要性を排除します。異なるアップグレーダブルコントラクトについて詳しく学ぶには、このOpenZeppelin ガイドを参照してください。

📋 要件

Foundry のインストール

このガイドでは Foundry のインストールが必要です。ターミナルウィンドウで以下を実行してください:

curl -L https://foundry.paradigm.xyz | bash;

foundryup;
# foundryup installs the 'forge' and 'cast' binaries, used later

より詳細なインストール手順については、Foundry のインストールガイドを参照してください。Berachain での Foundry の使用についての詳細は、このガイドを参照してください。

👨‍💻 アップグレーダブルコントラクトプロジェクトの作成

ステップ 1:プロジェクトのセットアップ

Foundry を初期化して(新しいプロジェクトフォルダを作成して)開発環境をセットアップしましょう:

forge init pol-upgrades --no-git --no-commit;

cd pol-upgrades;
# We observe the following basic layout
# .
# ├── foundry.toml
# ├── script
# │   └── Counter.s.sol
# ├── src
# │   └── Counter.sol
# └── test
#     └── Counter.t.sol

既存の Solidity コントラクトをすべて削除します:

# FROM: ./pol-upgrades

rm script/Counter.s.sol src/Counter.sol test/Counter.t.sol;

それでは、必要な OpenZeppelin の依存関係をインストールします:

# FROM: ./pol-upgrades


forge install OpenZeppelin/openzeppelin-contracts openzeppelin-contracts-upgradeable OpenZeppelin/openzeppelin-foundry-upgrades foundry-rs/forge-std --no-commit --no-git;

ステップ 2:Foundry の設定

プロジェクトのルートにremappings.txtファイルを作成し、以下の内容を記述します:

@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/

次に、foundry.tomlファイルを更新します:

[profile.default]
ffi = true
ast = true
build_info = true
evm_version = "cancun"
libs = ["lib"]
extra_output = ["storageLayout"]

ステップ 3:初期トークンコントラクトの作成

src/DeFiTokenV1.solというファイルを作成し、以下の内容を記述します:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract DeFiToken is ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address initialOwner) public initializer {
        __ERC20_init("DeFi Token", "DFT");
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();

        _mint(initialOwner, 1000000 * 10 ** decimals());
    }

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }

    function _authorizeUpgrade(
        address newImplementation
    ) internal override onlyOwner {}
}

このスマートコントラクトには、他の場所では見られないいくつかの特徴があることに気づくでしょう:

  • プロキシ化されたコントラクトはコンストラクタを使用しないため、コンストラクタのロジックは**initialize**関数に移動されています。これは、トークン名や所有者の設定など、通常のコントラクトのコンストラクタで実行される関数を実行します。
  • 継承しているERC20Ownableコントラクトは特別な「アップグレーダブル」バージョンで、(コンストラクタ外での)初期化やアップグレード時の再初期化を容易にします。

ステップ 4:デプロイメントスクリプトの作成

script/DeployProxy.s.solというファイルを作成し、以下の内容を記述します:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "../src/DeFiTokenV1.sol";
import "forge-std/Script.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

contract DeployProxy is Script {
    function run() public {
        vm.startBroadcast();

        address proxy = Upgrades.deployUUPSProxy(
            "DeFiTokenV1.sol:DeFiToken",
            abi.encodeCall(DeFiToken.initialize, (msg.sender))
        );

        vm.stopBroadcast();

        console.log("Proxy Address:", address(proxy));
        console.log("Token Name:", DeFiToken(proxy).name());
    }
}

ステップ 5:環境変数の設定

デプロイに進む前に、プロジェクトのルートに.envファイルを作成し、ウォレットのプライベートキーを追加します:

PK=your_private_key_here

その後、環境変数を読み込みます:

# FROM: ./pol-upgrades

source .env;

ステップ 6:トークンとプロキシのデプロイ

ここから興味深い部分が始まります。上記のDeployProxyスクリプトにおいて、OpenZeppelin のUpgrades.deployUUPSProxyの呼び出しは、実際には裏で 2 つのことを行います:

  1. DeFiToken実装コントラクトをデプロイする
  2. UUPSUpgradeableプロキシコントラクトをデプロイし、それをDeFiToken実装に接続する

まず、コントラクトをコンパイルすることから始めましょう:

# FROM: ./pol-upgrades

forge build;

次に、デプロイメントスクリプトを実行します(一貫性のために Solidity のバージョンを固定しています):

# FROM: ./pol-upgrades

forge script script/DeployProxy.s.sol --broadcast --rpc-url https://bartio.rpc.berachain.com/ --private-key $PK --use 0.8.25;


Example Deployment Output

ステップ 7:コントラクトの検証(オプション)

Beratrail エクスプローラーでコントラクトを検証したい場合は、以下のように実行します:

# FROM: ./pol-upgrades

forge verify-contract IMPLEMENTATION_ADDRESS ./src/DeFiTokenV1.sol:DeFiToken --verifier-url 'https://api.routescan.io/v2/network/testnet/evm/80084/etherscan' --etherscan-api-key "verifyContract" --num-of-optimizations 200 --compiler-version 0.8.25 --watch;

IMPLEMENTATION_ADDRESSをスクリプト出力で得られたアドレスに置き換えることを忘れないでください。プロキシの検証については心配する必要はありません。エクスプローラーはすでにこのコントラクトについて認識しているからです。

これで、Beratrail でプロキシコントラクトを確認すると、ERC20 トークンの属性がプロキシコントラクトに接続されているのが分かります(例としてデプロイされたプロキシコントラクトを参照してください)。

ステップ 8:アップグレードされたトークンコントラクトの作成

コード例のみを追いたい場合や、実装している PoL ロジックに興味がない場合は、次のコードブロックまでスキップしてください。

PoL イノベーション 💡

ここからアップグレーダブルコントラクトを使って創造的になれます。例えば、あなたのプロトコルに最も長く預けているユーザーにより多くの$BGT報酬を与えたいとします。しかし、預金トークンにはすでにアクティブなReward Vaultがあり、ユーザーにトークンを移行させたくありません。

アップグレード可能性がこれを解決します!例えば、ユーザーが従来のステーキングパラダイムで 100 トークンを持っていて、時間ブースト型システムに移行したいとします。60 日間ポジションを保持した後、PoL 獲得率が従来のステーキングを上回ることに気付くでしょう。

Traditional (V1) vs Time-boosted (V2) Rewards

src/DeFiTokenV2.solというファイルを作成し、以下の内容を記述します:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

interface IBerachainRewardsVault {
    function delegateStake(address account, uint256 amount) external;
    function delegateWithdraw(address account, uint256 amount) external;

    function getTotalDelegateStaked(
        address account
    ) external view returns (uint256);
}

/// @custom:oz-upgrades-from DeFiToken
contract DeFiTokenV2 is ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
    IBerachainRewardsVault public rewardsVault;
    uint256 public constant BONUS_RATE = 50; // 50% bonus per 30 days
    uint256 public constant BONUS_PERIOD = 30 days;
    mapping(address => uint256) public lastBonusTimestamp;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize() public reinitializer(2) {
        __ERC20_init("DeFi Token V2", "DFTV2");
    }

    function setRewardsVault(address _rewardsVault) external onlyOwner {
        rewardsVault = IBerachainRewardsVault(_rewardsVault);
    }

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }

    function applyBonus(address account) external {
        uint256 newBonusAmount = calculateBonus(account);
        require(newBonusAmount > 0, "No bonus to apply");

        // Mint new bonus tokens to this contract
        _mint(address(this), newBonusAmount);

        // Delegate new bonus stake
        rewardsVault.delegateStake(account, newBonusAmount);

        lastBonusTimestamp[account] = block.timestamp;
    }

    function calculateBonus(address account) public view returns (uint256) {
        uint256 userBalance = balanceOf(account);
        uint256 timeSinceLastBonus = block.timestamp -
            lastBonusTimestamp[account];
        return
            (userBalance * BONUS_RATE * timeSinceLastBonus) /
            (100 * BONUS_PERIOD);
    }

    function getBonusBalance(address account) public view returns (uint256) {
        return rewardsVault.getTotalDelegateStaked(account);
    }

    function removeBonus(address account) internal {
        uint256 bonusToRemove = getBonusBalance(account);
        if (bonusToRemove > 0) {
            rewardsVault.delegateWithdraw(account, bonusToRemove);
            _burn(address(this), bonusToRemove);
            lastBonusTimestamp[account] = 0;
        }
    }

    function transfer(
        address to,
        uint256 amount
    ) public override returns (bool) {
        removeBonus(msg.sender);
        lastBonusTimestamp[to] = block.timestamp;
        return super.transfer(to, amount);
    }

    function _authorizeUpgrade(
        address newImplementation
    ) internal override onlyOwner {}
}

なるほど、DeFiTokenV2にはかなりの変更があります。新しい内容を 2 つの部分に分けて説明しましょう。まず、アップグレード可能性に関連する部分を指摘し、次に新しい PoL メカニズムをサポートするためのスマートコントラクト関数について説明します。

アップグレード可能性の変更点

  • コントラクトには OpenZeppelin が要求する元のDeFiTokenコントラクトへの参照が含まれています。
  • コントラクトのinitialize関数にはreinitializer(2)修飾子があり、これが再初期化であることを示しています。2はコントラクトのバージョン番号を指します。

アップグレード可能性のみに関心がある場合は、次のセクションにスキップしてください。

PoL の変更点

こう思われるかもしれません:変更されないはずの ERC20 ポジションに紐づいているのに、Reward Vault コントラクトの残高がどうやって増えるのだろう?

いい質問です!時間ブースト型報酬を実現するために、Reward Vault のdelegateStake機能を活用しています。これにより、スマートコントラクトがユーザーに代わってステーキングできます。これは、ここでの時間ブースト型報酬や、仮想/非 ERC20 ポジションを PoL と統合するなど、様々なユースケースに役立ちます。

時間ベースのロジックを適用するために、DeFiTokenV2コントラクトは今やユーザーのステーキングロジックを処理し、ユーザーは単にウォレットにトークンを保持するだけです。詳しく見ていきましょう!

  • setRewardsVaultは、プロトコルがユーザーのブーストをステーキングして$BGTを獲得する Reward Vault アドレスを設定できるようにします。
  • calculateBonusは、最後にボーナスが適用されてから、ユーザーに与えられる追加残高を計算します。
  • applyBonusは新しいトークンを発行し、特定のユーザーに代わってdelegateStakeを使用してステーキングします。これは、ユーザーが獲得したボーナスに基づいて行われます。
  • getBonusBalanceは、ユーザーのボーナス残高を Rewards Vault コントラクトに問い合わせます。
  • removeBonusは、Reward Vault のdelegateWithdraw関数を呼び出して、ユーザーのボーナス残高を引き出し/無効化し、そのトークンを焼却します。これは、ユーザーがトークンを転送した際のボーナスの損失を反映します。

このサンプルで、従来の PoL に代わる革新的な選択肢がどのようなものか、イメージがつかめたと思います。このコードは不完全で、本番環境での使用には適さないことに注意してください

ステップ 9:コントラクトのテスト

すべてのスマートコントラクトを記述したので、アップグレード可能な PoL 統合トークンコントラクトの動作をテストしましょう。

以下の機能をテストします:

  • コントラクトのアップグレードが正常に実行されることの確認
  • PoL ボーナスロジックが期待通りに機能することの確認

test/DeFiToken.t.solというファイルを作成し、以下の内容を記述します:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "forge-std/Test.sol";
import "../src/DeFiTokenV1.sol";
import "../src/DeFiTokenV2.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

contract MockRewardsVault {
    mapping(address => uint256) public delegatedStakes;

    function delegateStake(address account, uint256 amount) external {
        delegatedStakes[account] += amount;
    }

    function delegateWithdraw(address account, uint256 amount) external {
        require(
            delegatedStakes[account] >= amount,
            "Insufficient delegated stake"
        );
        delegatedStakes[account] -= amount;
    }

    function getTotalDelegateStaked(
        address account
    ) external view returns (uint256) {
        return delegatedStakes[account];
    }
}

contract DeFiTokenTest is Test {
    DeFiToken deFiToken;
    DeFiTokenV2 deFiTokenV2;
    ERC1967Proxy proxy;
    address owner;
    address user1;
    MockRewardsVault mockRewardsVault;

    function setUp() public {
        DeFiToken implementation = new DeFiToken();
        owner = vm.addr(1);
        user1 = vm.addr(2);

        vm.startPrank(owner);
        proxy = new ERC1967Proxy(
            address(implementation),
            abi.encodeCall(implementation.initialize, owner)
        );
        deFiToken = DeFiToken(address(proxy));
        vm.stopPrank();

        mockRewardsVault = new MockRewardsVault();
    }

    function testBoostedStakingFunctionality() public {
        testUpgradeToV2();

        vm.startPrank(owner);
        deFiTokenV2.setRewardsVault(address(mockRewardsVault));
        deFiTokenV2.mint(user1, 1000 * 1e18);
        vm.stopPrank();

        // Fast forward 15 days
        vm.warp(block.timestamp + 15 days);

        // Apply bonus for user1
        vm.prank(user1);
        deFiTokenV2.applyBonus(user1);

        // Check bonus balance (should be 25% of user's balance after 15 days)
        uint256 expectedBonus = (1000 * 1e18 * 25) / 100;
        assertApproxEqAbs(
            deFiTokenV2.getBonusBalance(user1),
            expectedBonus,
            1e15
        );

        // Fast forward another 30 days
        vm.warp(block.timestamp + 30 days);

        // Apply bonus again (should be 75% of user's balance)
        vm.prank(user1);
        deFiTokenV2.applyBonus(user1);
        expectedBonus = (1000 * 1e18 * 75) / 100;
        assertApproxEqAbs(
            deFiTokenV2.getBonusBalance(user1),
            expectedBonus,
            1e15
        );

        // Test bonus removal on transfer
        vm.prank(user1);
        deFiTokenV2.transfer(owner, 500 * 1e18);

        // Check that bonus is removed
        assertEq(deFiTokenV2.getBonusBalance(user1), 0);
    }

    function testUpgradeToV2() public {
        vm.startPrank(owner);
        Upgrades.upgradeProxy(
            address(proxy),
            "DeFiTokenV2.sol:DeFiTokenV2",
            abi.encodeCall(DeFiTokenV2.initialize, ())
        );
        vm.stopPrank();

        deFiTokenV2 = DeFiTokenV2(address(proxy));
        assertTrue(address(deFiTokenV2) == address(proxy));
    }
}

はい、テストコードは長いですが、コメントを読むことで各テストケースで何が起こっているかを理解できるはずです。

# FROM: ./pol-upgrades

forge clean && forge test;

# [EXAMPLE OUTPUT]:
# Ran 2 tests for test/DeFiToken.t.sol:DeFiTokenTest
# [PASS] testBoostedStakingFunctionality() (gas: 2032978)
# [PASS] testUpgradeToV2() (gas: 1904385)
# Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.62s (11.14s CPU time)

# Ran 1 test suite in 5.66s (5.62s CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

ステップ 10:アップグレードスクリプトの作成

コントラクトが正常に動作することを確認したので、アップグレードの準備をしましょう。script/DeployUpgrade.s.solというファイルを作成し、以下の内容を記述します:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "../src/DeFiTokenV2.sol";
import "forge-std/Script.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

contract DeployAndUpgrade is Script {
    function run() public {
        // Replace with your proxy address
        address proxy = 0x0000000000000000000000000000000000000000;
        vm.startBroadcast();

        Upgrades.upgradeProxy(
            proxy,
            "DeFiTokenV2.sol:DeFiTokenV2",
            abi.encodeCall(DeFiTokenV2.initialize, ())
        );

        vm.stopBroadcast();

        console.log("Token Name:", DeFiTokenV2(proxy).name());
    }
}

proxy変数をステップ 6 で得たプロキシアドレスに置き換えてください。

このスクリプトは、以下の手順でアップグレードプロセスを組み立てています:

  1. アップグレードされたDeFiTokenV2コントラクトをデプロイする
  2. プロキシの実装を新しいDeFiTokenV2に切り替える
  3. initializeを再度呼び出してトークンの名前を変更する

ステップ 11:アップグレードの実行

ビルドアーティファクトをクリーンにしてから、アップグレードスクリプトを実行します:

# FROM: ./pol-upgrades

forge clean;
forge script script/DeployUpgrade.s.sol --broadcast --rpc-url https://bartio.rpc.berachain.com/ --private-key $PK --use 0.8.25;

Beratrail エクスプローラーでプロキシコントラクトを確認してみてください。同じアドレスで、トークン名(通常は不変のプロパティ)が変更されているのが見えるはずです!

トークン名が変更されました!

まとめ

おめでとうございます!ここまで到達したあなたは、アップグレーダブルコントラクトがどのように機能し、どのようにあなたの利点となるかの基本を学んだことになります。さらに注意深く見ていた場合は、Proof-of-Liquidity を扱う革新的な方法についても学んだことでしょう。
このガイドに従うことで、Berachain 上にアップグレーダブルな ERC20 トークンを正常にデプロイしました。そして、プロキシコントラクトの実装をアップグレードして、その機能にクールな変更を加えることに成功しました 🎉


🐻 Full Code Repository

最終的なコードや他のガイドを見たい場合は、Berachain Upgradeable Contracts Guide Codeを参照してください。
Berachain GitHub Guides Repo
https://github.com/berachain/guides/tree/main/apps/pyth-oracle?ref=berachain.ghost.io

🛠️ さらに開発を進めたいですか?

Berachain でさらに開発を進めたい、より多くの実装例を見たいとお考えですか?私たちBerachain GitHub Guides Repo をぜひご覧ください。このリポジトリには、NextJS、Hardhat、Viem、Foundry など、様々な技術を使用した幅広い実装例が用意されています。

より詳細な情報を探求したい場合は、Berachain ドキュメントをご覧ください。

開発者サポートをお探しですか?

質問をするために、Berachain Discordサーバーに参加し、開発者チャンネルをチェックしてください。

❤️ この記事に対して愛を示すのを忘れないでください 👏🏼



【Sunrise とは】
Sunrise は Proof of Liquidity(PoL)と Fee Abstraction(手数料抽象化)を備えたデータ可用性レイヤーです。 私たちは DA の体験を再構築し、多様なエコシステムからのモジュラー型流動性を活用してロールアップを立ち上げています。

【Social Links】

【お問合せ】
Sunrise へのお問い合わせはこちらから 👉 Google Form

1080x360.jpeg

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0