本記事は下記の翻訳となります。
『Deploy an Upgradeable ERC20 Token on Berachain』
アップグレーダブルコントラクトの紹介
ブロックチェーン上のスマートコントラクトは、Berachain のような環境では一度デプロイされると通常は変更不可能です。これは確実性を提供する一方で、バグの修正や機能の追加、急速に変化する環境への適応などのためにコントラクトを更新する必要がある開発者にとっては課題となる可能性があります。アップグレーダブルなコントラクトは、不変性と柔軟性の間の解決策を提供します。
Proof-of-Liquidity とアップグレーダブル性
Berachain の革新的な Proof of Liquidity(PoL)コンセンサスメカニズムに参加するプロトコルは、通常、ユーザーに対して、自身のプロトコルへの預け入れを表すERC20 トークンをステーキングすることを要求します。これにより、Berachain のネイティブトークンである$BGT
の報酬を獲得できます。
プロトコルは最初、ステーキングモデルから始まるかもしれません。しかし、プロトコルの機能を変更せずに、報酬の仕組みを少し創造的にしたいと考えたらどうでしょうか?ここでは、アップグレーダブルなコントラクトがどのようにしてこれを実現できるかを探ってみます。
💡 トークンをマイグレーションせずに、PoL ステーキングメカニズムを変更する
アップグレーダブルコントラクトガイド - 概要
このガイドでは、Foundry と OpenZeppelin のアップグレーダブルコントラクトをを使用して、Berachain 上で ERC20 トークンを作成、デプロイ、アップグレードするプロセスを説明します。具体的には、以下の内容を実施します:
- ERC20 コントラクト(
v1 Implementation
)をデプロイする -
v1 Implementation
のロジックを継承する Proxy コントラクトをデプロイする - PoL 報酬の時間ベースのブーストを実装するための新機能を持つ、修正版 ERC20 コントラクト(
v2 Implementation
)をデプロイする - Proxy を
v2 Implementation
のロジックを使用するようにアップグレードする
アップグレーダブルスマートコントラクトの仕組み
「Proxy」や「Implementation」という言葉は馴染みがないかもしれないので、コードに入る前にいくつかの用語と概念を明確にしておきましょう。
-
Proxy
はユーザーが相互作用するコントラクトです。コントラクトのデータとステートを保存する責任がありますが、単なるシェルとして機能し、機能やロジックは含まれていません。それは以下の役割です... -
Implementation
コントラクト。Implementation
は、ユーザー向けのProxy
コントラクトのすべてのコントラクトロジックをホストしますが、コントラクトアドレスにデータを保存しません。
上の図で示されているように、Proxy
とImplementation
コントラクトは連携して動作します:
- まずユーザーが
Proxy
にcall
を行います。 - リクエストは
delegatecall
を使用して、関連するImplementation
コントラクトにルーティングされます。 -
Proxy
コントラクトの権限を持つ所有者は、異なるImplementation
コントラクト間で切り替えることができます - つまり、アップグレーダブルなのです!
アップグレーダブルコントラクトには様々な種類があります。このチュートリアルで使用されるアップグレーダブルコントラクトの種類はUUPS(Universal Upgradeable Proxy Standard)です。これはアップグレードロジックをImplementation
コントラクト自体に組み込みます。この設計によりコントラクトの構造が簡素化され、Proxy
のアップグレードを管理するための追加の Admin コントラクトが不要になります。アップグレーダブルコントラクトの異なる種類について詳しく学ぶには、OpenZeppelin ガイドを参照してください。
📋 要件
- Node
v20.11.0
以上 - pnpm
- Berachain bArtio ネットワークで設定されたウォレット
- そのウォレット内の
$BERA
または Berachain テストネットトークン — Berachain Faucetを参照してください
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
**関数に移動されています。これは、トークン名や所有者の設定など、通常のコントラクトのコンストラクタで実行される関数を実行します。 - 継承している
ERC20
とOwnable
コントラクトは特別な「アップグレーダブル」バージョンで、(コンストラクタ外での)初期化やアップグレード時の再初期化を容易にします。
ステップ 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 つのことを行っています:
-
DeFiToken
実装コントラクトをデプロイする -
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;
ステップ 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 で得たプロキシアドレスに置き換えてください。
このスクリプトは、以下の手順でアップグレードプロセスを組み立てています:
- アップグレードされた
DeFiTokenV2
コントラクトをデプロイする - プロキシの実装を新しい
DeFiTokenV2
に切り替える -
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 コントラクトを確認してください。驚くべきことに、同じアドレスで、トークン名(通常は不変のプロパティ)が変更されているのが確認できるはずです!
まとめ
おめでとうございます!ここまで到達できたなら、アップグレーダブルコントラクトの基本的な機能と、それがどのようにあなたに有利に働くかを学んだことになります。特に注意深く見ていたなら、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