5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BerachainでアップグレーダブルなERC20トークンをデプロイする [Berachain翻訳]

Last updated at Posted at 2024-09-11

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

image.png

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

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

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

Berachain の革新的な Proof of Liquidity(PoL)コンセンサスメカニズムに参加するプロトコルは、通常、ユーザーに対して、自身のプロトコルへの預け入れを表すERC20 トークンをステーキングすることを要求します。これにより、Berachain のネイティブトークンである$BGTの報酬を獲得できます。

プロトコルは最初、ステーキングモデルから始まるかもしれません。しかし、プロトコルの機能を変更せずに、報酬の仕組みを少し創造的にしたいと考えたらどうでしょうか?ここでは、アップグレーダブルなコントラクトがどのようにしてこれを実現できるかを探ってみます。

💡 トークンをマイグレーションせずに、PoL ステーキングメカニズムを変更する

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

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

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

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

「Proxy」や「Implementation」という言葉は馴染みがないかもしれないので、コードに入る前にいくつかの用語と概念を明確にしておきましょう。

  • Proxyはユーザーが相互作用するコントラクトです。コントラクトのデータとステートを保存する責任がありますが、単なるシェルとして機能し、機能やロジックは含まれていません。それは以下の役割です...
  • Implementationコントラクト。Implementationは、ユーザー向けのProxyコントラクトのすべてのコントラクトロジックをホストしますが、コントラクトアドレスにデータを保存しません。

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

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

アップグレーダブルコントラクトには様々な種類があります。このチュートリアルで使用されるアップグレーダブルコントラクトの種類はUUPS(Universal Upgradeable Proxy Standard)です。これはアップグレードロジックをImplementationコントラクト自体に組み込みます。この設計によりコントラクトの構造が簡素化され、Proxyのアップグレードを管理するための追加の Admin コントラクトが不要になります。アップグレーダブルコントラクトの異なる種類について詳しく学ぶには、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:トークンと Proxy のデプロイ

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

  1. DeFiToken実装コントラクトをデプロイする
  2. UUPSUpgradeable proxy コントラクトをデプロイし、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:コントラクトの Verify(オプション)

Beratrail Explorerでコントラクトを Verify したい場合は、以下のように実行します:

# 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をスクリプト出力のものに置き換えることを忘れないでください。proxy の検証については心配する必要はありません。エクスプローラーはすでにこのコントラクトについて把握しているからです。

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

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

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

PoL イノベーション 💡

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

Upgradeability がこれを解決します!例えば、従来のステーキングパラダイムでユーザーが 100 トークンを持っていて、時間ブースト型システム(time-boosted system)に移行したい場合を考えてみましょう。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 つの部分に分けて説明しましょう。最初に、Upgradeability に関連する部分を指摘します。次に、新しい PoL メカニズムをサポートするためのスマートコントラクト関数について説明します。

Upgradeability の変更点

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

Upgradeability の変更点

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

Upgradeability のみに関心がある場合は、次のセクションにスキップしてください。

PoL 変更点

*「Reward Vault コントラクトの残高が、変更されるべきではない ERC20 ポジションに紐づいているのに、どうやって増加するのだろう?」*と疑問に思うかもしれません。

良い質問です!時間ベースのブースト報酬を実現するために、Reward Vault のdelegateStake機能を活用します。これにより、スマートコントラクトがユーザーに代わってステーキングを行うことができます。これは、ここでの時間ブースト報酬や、virtual/non-ERC20 position を 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:アップグレードの実行

ビルドアーティファクト(build artifacts)をクリーンアップし、その後アップグレードスクリプトを実行します:

# 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 Explorer で Proxy コントラクトを確認してください。驚くべきことに、同じアドレスで、トークン名(通常は不変のプロパティ)が変更されているのが確認できるはずです!


The token name changed!

まとめ

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

🐻 完全なコードリポジトリ

最終的なコードを確認したり、他のガイドを見たい場合は、Berachain Upgradeable Contracts Guide Codeをチェックしてください。

🛠️ もっとビルドしたいですか?

Berachain 上でさらに開発を進めたり、より多くの例を見たい場合は、Berachain GitHub Guides Repo をご覧ください。NextJS、Hardhat、Viem、Foundry など、様々な実装例が幅広く用意されています。

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

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

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



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

【Social Links】

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

1080x360.jpeg

5
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
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?